-
Notifications
You must be signed in to change notification settings - Fork 3
Tag update #326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Tag update #326
Conversation
Reviewer's GuideThis PR refactors the Tag domain by introducing hierarchical grouping support (isGroup) with a unified validation flow, removing legacy contentType/order fields, simplifying create/update/remove operations using new relatedIds utilities, updating GraphQL schemas/queries/mutations accordingly, adjusting database definitions and core types, and adding migration scripts for existing data. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds a migrateTags script; updates migrateContentType to close Mongo client and exit; reshapes the tags domain (DB schema, model, utils, GraphQL, TRPC, core-types) to introduce isGroup/relatedIds, change ordering/indexes and filters; makes document.code optional; and updates frontend tag UI, queries, types, and context-driven flows. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant GQL as GraphQL Resolver
participant Model as Tags Model
participant DB as MongoDB
participant Util as Tag Utils
Client->>GQL: tagsAdd(name, colorCode, parentId, isGroup, description)
GQL->>Model: createTag(doc)
Model->>Model: validate(null, doc)
Model->>DB: insertOne(doc)
DB-->>Model: inserted tag
Model->>Util: setRelatedTagIds(tag)
Util->>DB: updateOne(parentId, set deduped relatedIds)
alt parent has parent
Util->>Util: recurse setRelatedTagIds(parent)
end
Model-->>GQL: created tag
GQL-->>Client: Tag
sequenceDiagram
autonumber
actor Client
participant GQL as Tags Query
participant DB as MongoDB
Client->>GQL: tags({ isGroup, parentId, ids, excludeIds, instanceId })
GQL->>GQL: build filter\n- apply isGroup\n- parse type+instanceId\n- expand parentId->descendants\n- apply ids/excludeIds
GQL->>DB: find(filter).paginate(orderBy: { order:1 })
DB-->>GQL: results
GQL-->>Client: Tag list
sequenceDiagram
autonumber
participant Script as migrateTags.ts
participant Mongo as MongoDB
Script->>Mongo: connect(MONGO_URL)
Script->>Mongo: updateMany({}, { $unset: { scopeBrandIds }, $set: { isGroup:false, parentId:"", order:"" } })
Mongo-->>Script: result
Script->>Mongo: close()
Script-->>Script: process.exit()
sequenceDiagram
autonumber
participant Script as migrateContentType.ts
participant Mongo as MongoDB
Script->>Mongo: connect(MONGO_URL)
Script->>Mongo: process collections...
Script-->>Script: log finish timestamp
Script->>Mongo: await client.close()
Script-->>Script: process.exit()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–90 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes - here's some feedback:
- In updateTag, you call setRelatedTagIds with the pre-update
tagobject instead of theupdateddocument, which may skip adding/removing related IDs correctly—swap to usingupdatedthere. - The validate method contains duplicate checks for
isGroupandparentId—consider consolidating those into a single guard to reduce redundancy. - Since you’re frequently filtering by
isGroup, adding an index on that field in the schema could improve query performance for group vs non-group lookups.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In updateTag, you call setRelatedTagIds with the pre-update `tag` object instead of the `updated` document, which may skip adding/removing related IDs correctly—swap to using `updated` there.
- The validate method contains duplicate checks for `isGroup` and `parentId`—consider consolidating those into a single guard to reduce redundancy.
- Since you’re frequently filtering by `isGroup`, adding an index on that field in the schema could improve query performance for group vs non-group lookups.
## Individual Comments
### Comment 1
<location> `backend/core-api/src/modules/tags/db/models/Tags.ts:88` </location>
<code_context>
- const childCount = await models.Tags.countDocuments({
- parentId: _id,
- });
+ const childTagIds = await models.Tags.find({ parentId: _id }).distinct(
+ '_id',
+ );
- if (childCount > 0) {
</code_context>
<issue_to_address>
Removing parentId from child tags on tag removal may leave orphaned tags.
Currently, child tags become orphaned when their parent is removed. Please review if this is intentional, or if child tags should be deleted or reassigned to maintain data integrity.
</issue_to_address>
### Comment 2
<location> `backend/core-api/src/commands/migrateTags.ts:26` </location>
<code_context>
+ Tags = db.collection('tags');
+
+ try {
+ await Tags.updateMany(
+ {},
+ {
+ $unset: { type: '', objectCount: '', order: '', scopeBrandIds: '' },
+ $set: { isGroup: false, parentId: '' }, // flatten parentId: no nested parent tags allowed
+ },
+ );
+ } catch (e) {
</code_context>
<issue_to_address>
Migration script sets parentId to empty string, which may not be valid for all usages.
Downstream code may expect parentId to be null or undefined when unset. Consider using null or omitting the field instead of assigning an empty string.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
$set: { isGroup: false, parentId: '' }, // flatten parentId: no nested parent tags allowed
=======
$set: { isGroup: false, parentId: null }, // flatten parentId: no nested parent tags allowed
>>>>>>> REPLACE
</suggested_fix>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| createdAt: new Date(), | ||
| }); | ||
| const tag = await models.Tags.create(doc); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): Removing parentId from child tags on tag removal may leave orphaned tags.
Currently, child tags become orphaned when their parent is removed. Please review if this is intentional, or if child tags should be deleted or reassigned to maintain data integrity.
| {}, | ||
| { | ||
| $unset: { type: '', objectCount: '', order: '', scopeBrandIds: '' }, | ||
| $set: { isGroup: false, parentId: '' }, // flatten parentId: no nested parent tags allowed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): Migration script sets parentId to empty string, which may not be valid for all usages.
Downstream code may expect parentId to be null or undefined when unset. Consider using null or omitting the field instead of assigning an empty string.
| $set: { isGroup: false, parentId: '' }, // flatten parentId: no nested parent tags allowed | |
| $set: { isGroup: false, parentId: null }, // flatten parentId: no nested parent tags allowed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Changes requested ❌
Reviewed everything up to a28c2ca in 2 minutes and 2 seconds. Click for details.
- Reviewed
677lines of code in11files - Skipped
0files when reviewing. - Skipped posting
5draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. backend/core-api/src/modules/documents/db/definitions/documents.ts:11
- Draft comment:
Verify that 'optional: true' is supported. Typically, 'required: false' is used in Mongoose schemas. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 20% vs. threshold = 50% While the comment is technically correct that 'required: false' is more common in Mongoose, the codebase already uses 'optional: true' pattern. The comment suggests changing established patterns which could lead to inconsistency. Since the existing code works (as evidenced by subType field), this is more of a style preference than a real issue. The comment might be pointing out a genuine Mongoose best practice that the team should follow. Maybe there's a reason they should switch to the standard pattern. Consistency within a codebase is more important than following external conventions, especially when the current pattern is already working. Changing this would create inconsistency with the subType field. Delete the comment. The suggested change would create inconsistency with existing code patterns, and the current syntax is already proven to work in this codebase.
2. backend/core-api/src/modules/tags/graphql/mutations.ts:44
- Draft comment:
The 'tagsTag' mutation still checks the 'type' field, which has been removed from the tag schema. Remove or update this check. - Reason this comment was not posted:
Comment was not on a location in the diff, so it can't be submitted as a review comment.
3. backend/core-api/src/modules/tags/graphql/queries.ts:84
- Draft comment:
Remove or update the 'type' filter in tagsQueryCount, since the 'type' field is no longer part of the tag schema. - Reason this comment was not posted:
Comment was not on a location in the diff, so it can't be submitted as a review comment.
4. backend/core-api/src/modules/tags/utils.ts:58
- Draft comment:
Avoid marking the forEach callback as async if no asynchronous operations (await) are used within it. - Reason this comment was not posted:
Comment was not on a location in the diff, so it can't be submitted as a review comment.
5. backend/core-api/src/commands/migrateContentType.ts:36
- Draft comment:
Typographical note: In the comment, consider changing "no longer include a contentType field" to "no longer includes a contentType field" for proper subject-verb agreement. - Reason this comment was not posted:
Comment did not seem useful. Confidence is useful =0%<= threshold50%This comment is purely informative and suggests a typographical change in a comment, which doesn't impact the functionality or logic of the code. It doesn't align with the rules for useful comments in a pull request review.
Workflow ID: wflow_SsExF6uj2BBG4UOl
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
|
|
||
| console.log(`Process finished at: ${new Date().toISOString()}`); | ||
|
|
||
| process.exit(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Call client.close() before process.exit() for proper cleanup of the MongoDB connection.
| process.exit(); | |
| await client.close(); process.exit(); |
| console.log(`Error occurred: ${e.message}`); | ||
| await client.close(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure client connection is closed on success too (e.g., in a finally block) to avoid leaving open connections.
|
|
||
| if (updated) { | ||
| await setRelatedTagIds(models, updated); | ||
| await setRelatedTagIds(models, tag); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass the updated tag instead of the original tag when calling setRelatedTagIds.
| await setRelatedTagIds(models, tag); | |
| await setRelatedTagIds(models, updated); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important
Looks good to me! 👍
Reviewed ac262e5 in 49 seconds. Click for details.
- Reviewed
37lines of code in3files - Skipped
0files when reviewing. - Skipped posting
3draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. backend/core-api/src/commands/migrateContentType.ts:61
- Draft comment:
Consider using a try/finally block for client.close() to avoid duplicate close calls on both success and error paths. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
2. backend/core-api/src/commands/migrateTags.ts:40
- Draft comment:
Consider refactoring to use a finally block to ensure client.close() is called once and avoid redundant invocation. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
3. backend/core-api/src/modules/tags/db/models/Tags.ts:79
- Draft comment:
Passing the 'updated' document to setRelatedTagIds ensures related IDs are based on the new state; this change is correct. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
Workflow ID: wflow_Lf9Oq8oIS7YXtnuE
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
backend/core-api/src/modules/tags/utils.ts (1)
41-42: Bug: $in expects an array; current query will not match as intended.Use a direct match or wrap in an array.
Apply:
- const tags = await models.Tags.find({ relatedIds: { $in: tag._id } }); + const tags = await models.Tags.find({ relatedIds: tag._id }); + // or: { relatedIds: { $in: [tag._id] } }backend/core-api/src/modules/tags/graphql/queries.ts (1)
71-93: tagsQueryCount still relies on removed "type" field.Schema removed type; this path will return wrong counts. Replace with isGroup and searchValue filters.
- async tagsQueryCount( - _parent: undefined, - { - type, - searchValue, - }: { - type: string; - searchValue?: string; - }, - { models, commonQuerySelector }: IContext, - ) { + async tagsQueryCount( + _parent: undefined, + { searchValue, isGroup }: { searchValue?: string; isGroup?: boolean }, + { models, commonQuerySelector }: IContext, + ) { const selector: any = { ...commonQuerySelector }; - if (type) { - selector.type = type; - } + if (typeof isGroup === 'boolean') { + selector.isGroup = isGroup; + } if (searchValue) { - selector.name = new RegExp(`.*${searchValue}.*`, 'i'); + const ESCAPE_RE = /[.*+?^${}()|[\]\\]/g; + const safe = String(searchValue).slice(0, 100).replace(ESCAPE_RE, '\\$&'); + selector.name = { $regex: safe, $options: 'i' }; } return models.Tags.countDocuments(selector); },
🧹 Nitpick comments (10)
backend/core-api/src/commands/migrateContentType.ts (1)
63-64: Avoid process.exit(); close the Mongo client in a finally block and let Node exit naturally.Prevents abrupt termination and ensures connections/logs flush.
Apply:
- console.log(`Process finished at: ${new Date().toISOString()}`); - - process.exit(); + console.log(`Process finished at: ${new Date().toISOString()}`); }; -command(); +command() + .catch((e) => { + console.error(`Fatal error: ${e?.message || e}`); + process.exitCode = 1; + }) + .finally(async () => { + try { + await client.close(); + } catch {} + });backend/core-api/src/commands/migrateTags.ts (1)
34-41: Close the client in finally; don’t rely on process.exit().Current flow closes the client only on error and then calls process.exit() on success.
Apply:
- } catch (e) { - console.log(`Error occurred: ${e.message}`); - await client.close(); - } - - console.log(`Process finished at: ${new Date().toISOString()}`); - - process.exit(); + } catch (e) { + console.error(`Error occurred: ${e.message}`); + process.exitCode = 1; + } finally { + try { + await client.close(); + } catch {} + console.log(`Process finished at: ${new Date().toISOString()}`); + }backend/core-api/src/modules/tags/utils.ts (3)
29-33: Avoid refetch when you already have parentTag; recurse via parentTag to save one read.Function only needs the ancestor chain, not the updated relatedIds snapshot.
Apply:
- const updated = await models.Tags.findOne({ _id: tag.parentId }); - - if (updated) { - await setRelatedTagIds(models, updated); - } + if (parentTag.parentId) { + const grand = await models.Tags.findOne({ _id: parentTag.parentId }); + if (grand) await setRelatedTagIds(models, grand); + }
47-50: Avoid mutating the input tag’s relatedIds reference.Clone before push to prevent unintended side effects.
Apply:
- const relatedIds: string[] = tag.relatedIds || []; - - relatedIds.push(tag._id); + const relatedIds: string[] = [...(tag.relatedIds || []), tag._id];
58-67: Remove unnecessary async in forEach and prefer for..of for clarity.No awaits inside; async adds confusion.
Apply:
- tags.forEach(async (t) => { - const ids = (t.relatedIds || []).filter((id) => !relatedIds.includes(id)); - doc.push({ - updateOne: { - filter: { _id: t._id }, - update: { $set: { relatedIds: ids } }, - }, - }); - }); + for (const t of tags) { + const ids = (t.relatedIds || []).filter((id) => !relatedIds.includes(id)); + doc.push({ + updateOne: { + filter: { _id: t._id }, + update: { $set: { relatedIds: ids } }, + }, + }); + }backend/erxes-api-shared/src/core-types/modules/tags/tag.ts (1)
7-9: Align document shape with schema timestamps and clarify optionality.Add updatedAt to ITagDocument (timestamps: true). Keep isGroup optional on input (ITag) but it’s always present on documents.
export interface ITagDocument extends ITag, Document { _id: string; createdAt: Date; + updatedAt: Date; }backend/core-api/src/modules/tags/graphql/queries.ts (1)
26-41: Avoid shadowing params and improve readability.Local ids shadows params.ids; rename and keep intent clear.
- let ids = [parentId, ...parentTag]; + let descendantIds = [parentId, ...parentTag]; @@ - if (childTag.length > 0) { - ids = [...ids, ...childTag]; + if (childTag.length > 0) { + descendantIds = [...descendantIds, ...childTag]; await getChildTags(childTag); } @@ - filter._id = { $in: ids }; + filter._id = { $in: descendantIds };backend/core-api/src/modules/tags/db/definitions/tags.ts (1)
11-12: Label may mislead: these are descendant IDs, not just direct children.setRelatedTagIds propagates transitive IDs upward. Consider renaming the label to avoid confusion.
- relatedIds: { type: [String], label: 'Children tag ids' }, + relatedIds: { type: [String], label: 'Descendant tag ids' },backend/core-api/src/modules/tags/graphql/schemas.ts (2)
43-43: Prefer an input object for mutation paramsUsing an input object improves evolvability (additive changes, deprecations) vs. spreading scalars.
Proposed additions outside this hunk:
--- a/backend/core-api/src/modules/tags/graphql/schemas.ts +++ b/backend/core-api/src/modules/tags/graphql/schemas.ts @@ export const types = ` type Tag @key(fields: "_id") @cacheControl(maxAge: 3) { @@ } + + input TagMutationParams { + colorCode: String + parentId: String + isGroup: Boolean + } `; -const mutationParams = ` - colorCode: String, - parentId: String, - isGroup: Boolean, -`; +const mutationParams = `params: TagMutationParams`;
4-13: Make_idandnamenon-nullable; verify Date scalar registrationMark
_idandnameasString!if always present, and confirm theDatescalar is registered globally (e.g. viaapolloCommonTypes/apolloCustomScalars). KeepcreatedAt: Datenullable only if older records may lack it.type Tag @key(fields: "_id") @cacheControl(maxAge: 3) { - _id: String - name: String + _id: String! + name: String! colorCode: String parentId: String relatedIds: [String] isGroup: Boolean createdAt: Date }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (11)
backend/core-api/src/commands/migrateContentType.ts(2 hunks)backend/core-api/src/commands/migrateTags.ts(1 hunks)backend/core-api/src/modules/documents/db/definitions/documents.ts(1 hunks)backend/core-api/src/modules/tags/@types/tag.ts(1 hunks)backend/core-api/src/modules/tags/db/definitions/tags.ts(1 hunks)backend/core-api/src/modules/tags/db/models/Tags.ts(3 hunks)backend/core-api/src/modules/tags/graphql/mutations.ts(0 hunks)backend/core-api/src/modules/tags/graphql/queries.ts(2 hunks)backend/core-api/src/modules/tags/graphql/schemas.ts(2 hunks)backend/core-api/src/modules/tags/utils.ts(1 hunks)backend/erxes-api-shared/src/core-types/modules/tags/tag.ts(1 hunks)
💤 Files with no reviewable changes (1)
- backend/core-api/src/modules/tags/graphql/mutations.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx}: Use functional and declarative programming patterns; avoid classes.
Use TypeScript for all code; prefer interfaces over types.
Avoid enums; use maps instead.
Files:
backend/core-api/src/modules/documents/db/definitions/documents.tsbackend/core-api/src/modules/tags/@types/tag.tsbackend/core-api/src/commands/migrateContentType.tsbackend/core-api/src/commands/migrateTags.tsbackend/core-api/src/modules/tags/db/definitions/tags.tsbackend/core-api/src/modules/tags/utils.tsbackend/core-api/src/modules/tags/graphql/queries.tsbackend/core-api/src/modules/tags/db/models/Tags.tsbackend/erxes-api-shared/src/core-types/modules/tags/tag.tsbackend/core-api/src/modules/tags/graphql/schemas.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Avoid console logs.
Always use absolute paths when importing.
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
Use absolute path when import.
Files:
backend/core-api/src/modules/documents/db/definitions/documents.tsbackend/core-api/src/modules/tags/@types/tag.tsbackend/core-api/src/commands/migrateContentType.tsbackend/core-api/src/commands/migrateTags.tsbackend/core-api/src/modules/tags/db/definitions/tags.tsbackend/core-api/src/modules/tags/utils.tsbackend/core-api/src/modules/tags/graphql/queries.tsbackend/core-api/src/modules/tags/db/models/Tags.tsbackend/erxes-api-shared/src/core-types/modules/tags/tag.tsbackend/core-api/src/modules/tags/graphql/schemas.ts
🧬 Code graph analysis (3)
backend/core-api/src/modules/tags/graphql/queries.ts (1)
backend/core-api/src/modules/tags/@types/tag.ts (1)
ITagFilterQueryParams(6-15)
backend/core-api/src/modules/tags/db/models/Tags.ts (2)
backend/erxes-api-shared/src/core-types/modules/tags/tag.ts (1)
ITag(3-9)backend/core-api/src/modules/tags/utils.ts (1)
setRelatedTagIds(5-34)
backend/core-api/src/modules/tags/graphql/schemas.ts (1)
backend/erxes-api-shared/src/utils/apollo/constants.ts (1)
GQL_CURSOR_PARAM_DEFS(1-9)
🪛 ast-grep (0.38.6)
backend/core-api/src/modules/tags/graphql/queries.ts
[warning] 11-11: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(.*${searchValue}.*, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (7)
backend/core-api/src/modules/tags/@types/tag.ts (1)
12-12: isGroup integration verified
All GraphQL schemas (schemas.ts:10,27,43) and the query resolver (queries.ts:19–21) support isGroup; the DB definition (db/definitions/tags.ts:12) defaults it to false, preserving existing behavior when omitted; no REST endpoints to update.backend/core-api/src/commands/migrateContentType.ts (1)
36-37: LGTM: Skipping tags here is correct given tags no longer carry contentType.backend/core-api/src/commands/migrateTags.ts (1)
30-31: Are you sure all tags should become isGroup: false?If you had actual “group” tags, this erases that classification. Confirm intent; otherwise, derive isGroup during migration.
backend/core-api/src/modules/tags/utils.ts (1)
6-14: LGTM: Early exits reduce needless queries.backend/core-api/src/modules/tags/graphql/queries.ts (1)
100-101: Good switch to model-layer getter.Routing tagDetail through getTag centralizes not-found handling.
backend/core-api/src/modules/tags/db/models/Tags.ts (2)
60-67: LGTM: createTag flow is simpler and consistent.Validation, create, and propagation ordering looks good.
85-100: LGTM: removeTag unsets children and cleans up ancestry.Child parentId unset + removeRelatedTagIds before deleteOne is sound.
| await Tags.updateMany( | ||
| {}, | ||
| { | ||
| $unset: { type: '', objectCount: '', order: '', scopeBrandIds: '' }, | ||
| $set: { isGroup: false, parentId: '' }, // flatten parentId: no nested parent tags allowed | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flattening parentId and enforcing unique name can collide; preflight duplicate names before migration.
If name is now unique (per tags schema), flattening to parentId: '' may create duplicate names that will break index creation or future writes.
- Preflight check duplicates and decide merge/rename rules before running at scale.
Example mongosh precheck:
db.tags.aggregate([
{ $group: { _id: "$name", c: { $sum: 1 }, ids: { $push: "$_id" } } },
{ $match: { c: { $gt: 1 } } },
{ $sort: { c: -1 } }
])Optionally migrate with a pipeline to compute fields atomically:
- await Tags.updateMany(
- {},
- {
- $unset: { type: '', objectCount: '', order: '', scopeBrandIds: '' },
- $set: { isGroup: false, parentId: '' }, // flatten parentId: no nested parent tags allowed
- },
- );
+ // Compute isGroup (if derivable) and flatten, then unset legacy fields
+ await Tags.updateMany(
+ {},
+ [
+ { $set: { parentId: '' } },
+ { $set: { isGroup: false } }, // or derive if you still have a marker field
+ { $unset: ['type', 'objectCount', 'order', 'scopeBrandIds'] },
+ ],
+ );| content: { type: String }, | ||
| replacer: { type: String }, | ||
| code: { type: String }, | ||
| code: { type: String, optional: true }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Unique index on an optional field will cause duplicate-key errors; use a partial index and drop "optional".
- MongoDB unique indexes on non-present/null values are tricky; with the current definition, inserts can collide on null/absent code. Also, Mongoose does not recognize "optional"; use required: false (or omit) and a partialFilterExpression to index only docs that have a code.
Apply:
- code: { type: String, optional: true },
+ code: { type: String, required: false, trim: true },
-documentSchema.index({ code: 1 }, { unique: true });
+documentSchema.index(
+ { code: 1 },
+ { unique: true, partialFilterExpression: { code: { $exists: true } } }
+);Verification (run in mongosh against the target DB):
Also applies to: 18-18
🏁 Script executed:
db.getCollection('documents').getIndexes()
# Expect the 'code_1' index to have { unique: true, partialFilterExpression: { code: { $exists: true } } }Length of output: 235
🏁 Script executed:
#!/bin/bash
# Print the 'code' field definition and surrounding lines
rg -n "code:\s*\{" -C2 backend/core-api/src/modules/documents/db/definitions/documents.ts
# Print all index definitions on documentSchema
rg -n "documentSchema\.index" -C3 backend/core-api/src/modules/documents/db/definitions/documents.tsLength of output: 398
Use required:false (not optional) and add a partial filter to the unique ‘code’ index
Replace the field and index definitions as follows:
- code: { type: String, optional: true },
+ code: { type: String, required: false, trim: true },
- documentSchema.index({ code: 1 }, { unique: true });
+ documentSchema.index(
+ { code: 1 },
+ { unique: true, partialFilterExpression: { code: { $exists: true } } }
+ );After deployment, in the Mongo shell run:
db.getCollection('documents').getIndexes();and confirm that the code_1 index includes your partialFilterExpression.
🤖 Prompt for AI Agents
In backend/core-api/src/modules/documents/db/definitions/documents.ts around
line 11, change the field definition from optional:true to required:false and
update the unique index on code to include a partialFilterExpression so the
unique constraint only applies when code exists (e.g. partialFilterExpression: {
code: { $exists: true } }), then deploy and run
db.getCollection('documents').getIndexes() in the Mongo shell to confirm the
code_1 index includes the partialFilterExpression.
| label: 'Type', | ||
| index: true, | ||
| }, | ||
| name: { type: String, label: 'Name', unique: true }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Global unique on name is risky; scope uniqueness.
unique: true enforces cross-tenant/global uniqueness and blocks same-name tags under different groups. Prefer a compound unique index (name, parentId) or include tenant scope (e.g., organizationId).
- name: { type: String, label: 'Name', unique: true },
+ name: { type: String, label: 'Name' },
@@
-tagSchema.index({ _id: 1, name: 1, parentId: 1 });
+tagSchema.index({ _id: 1, name: 1, parentId: 1 });
+// Enforce uniqueness only within the same parent (adjust to include tenant scope if applicable)
+tagSchema.index({ name: 1, parentId: 1 }, { unique: true });If a unique index on name already exists in prod, we’ll need a migration to drop it before adding the compound index. I can prepare it.
🏁 Script executed:
#!/bin/bash
# Locate the tags.ts schema definition
fd -t f tags.ts
# Preview the top of the file to see field definitions
sed -n '1,200p' backend/core-api/src/modules/tags/db/definitions/tags.ts
# Inspect the schema object and index declarations
rg -n "tagSchema" -C3 backend/core-api/src/modules/tags/db/definitions/tags.ts
rg -n "index" -C3 backend/core-api/src/modules/tags/db/definitions/tags.ts
# Check for tenant or hierarchical scope fields
rg -n "parentId" -C3 backend/core-api/src/modules/tags/db/definitions/tags.ts
rg -n "organizationId" -C3 backend/core-api/src/modules/tags/db/definitions/tags.tsLength of output: 2612
Scope tag name uniqueness to its parent (and tenant)
- In backend/core-api/src/modules/tags/db/definitions/tags.ts (line 8), remove the field-level
unique: trueonname. - Add a compound unique index:
(Include tenant/organizationId in the index if this collection is multi-tenant.)
tagSchema.index({ name: 1, parentId: 1 }, { unique: true });
- If a standalone unique index on
namealready exists in production, drop it via a migration before creating the new compound index.
| const { type, searchValue, tagIds, parentId, ids, excludeIds } = params; | ||
| const { searchValue, parentId, ids, excludeIds, isGroup } = params; | ||
|
|
||
| const filter: FilterQuery<ITagFilterQueryParams> = { ...commonQuerySelector }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use correct FilterQuery generic (document type, not params).
FilterQuery should target ITagDocument.
-import { FilterQuery } from 'mongoose';
+import { FilterQuery } from 'mongoose';
+import { ITagDocument } from 'erxes-api-shared/core-types';
@@
- const filter: FilterQuery<ITagFilterQueryParams> = { ...commonQuerySelector };
+ const filter: FilterQuery<ITagDocument> = { ...commonQuerySelector };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const filter: FilterQuery<ITagFilterQueryParams> = { ...commonQuerySelector }; | |
| import { FilterQuery } from 'mongoose'; | |
| import { ITagDocument } from 'erxes-api-shared/core-types'; | |
| @@ | |
| const filter: FilterQuery<ITagDocument> = { ...commonQuerySelector }; |
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/graphql/queries.ts around line 9, the
FilterQuery generic is incorrectly using ITagFilterQueryParams; change it to use
the document type ITagDocument (i.e., FilterQuery<ITagDocument>) so the query is
typed against the Mongo document shape rather than the filter params; update the
import/usage accordingly and ensure any related types or helper selectors match
the ITagDocument shape.
| if (isGroup) { | ||
| filter.isGroup = isGroup; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Allow explicit isGroup=false filtering.
Current check skips false. Use strict boolean guard.
-if (isGroup) {
- filter.isGroup = isGroup;
-}
+if (typeof isGroup === 'boolean') {
+ filter.isGroup = isGroup;
+}🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/graphql/queries.ts around lines 19 to 21,
the current conditional uses "if (isGroup)" which skips when isGroup is
explicitly false; change the guard to detect an explicit boolean (e.g., if
(typeof isGroup === 'boolean') or if (isGroup !== undefined)) and then assign
filter.isGroup = isGroup so that false values are preserved.
| parentId: String, | ||
| ids: [String], | ||
| excludeIds: Boolean, | ||
| isGroup: Boolean, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent API: tagsQueryCount still uses removed “type” argument
type was removed elsewhere; tagsQueryCount still declares it. Align with the new isGroup filter to avoid schema/implementation drift.
- tagsQueryCount(type: String, searchValue: String): Int
+ tagsQueryCount(searchValue: String, isGroup: Boolean): IntPlease also update the resolver signature accordingly and adjust callers.
Also applies to: 37-37
| tagsAdd(name: String!, ${mutationParams}): Tag | ||
| tagsEdit(_id: String!, name: String, ${mutationParams}): Tag | ||
| tagsTag(targetIds: [String!]!, tagIds: [String!]!): JSON |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Tighten mutation payload types and confirm resolver parity
- Consider returning a typed payload instead of
JSONfortagsTag(e.g.,TagTagResult { modifiedCount: Int, skippedCount: Int }). - Double-check that mutation resolvers were updated to accept
nameat the top level and the removal oftype.
🏁 Script executed:
#!/bin/bash
# Verify mutations/resolvers alignment
rg -nP 'mutation[s]?\s*=\s*`|export const mutations' -C2 backend/core-api/src/modules/tags/graphql
rg -nP '\btags(Add|Edit|Tag)\b' -C3 backend/core-api/src/modules/tags/graphql
rg -nP '\btags(Add|Edit|Tag)\b' -C3 backend/core-api/src/modules/tags/graphql/*.ts backend/core-api/src/modules/tags/graphql/**/*.ts
# Look for lingering usage of the old "type" argument
rg -nP '\btagsQueryCount\s*\(.*type\s*:' -n backend -C2Length of output: 6001
🏁 Script executed:
#!/bin/bash
# Display the tagsTag resolver to verify argument structure and usage of "type"
sed -n '1,200p' backend/core-api/src/modules/tags/graphql/mutations.tsLength of output: 2224
🏁 Script executed:
#!/bin/bash
# Display the start of the schema file to inspect mutationParams
sed -n '1,200p' backend/core-api/src/modules/tags/graphql/schemas.tsLength of output: 1120
Fix schema/resolver mismatch for tagsTag and tighten its return type
- The
tagsTagmutation in schemas.ts (line 49) no longer declares atype: String!argument, but the resolver still destructures and usestype. Either re-addtype: String!to the mutation signature or remove its usage in backend/core-api/src/modules/tags/graphql/mutations.ts (around line 27). - (Optional) Replace the
JSONreturn type ontagsTagwith a concrete GraphQL payload (e.g.type TagTagResult { modifiedCount: Int!, skippedCount: Int! }).
| const relatedIds: string[] = [tag._id, ...(tag.relatedIds || [])]; | ||
|
|
||
| await models.Tags.updateOne( | ||
| { _id: parentTag._id }, | ||
| { $set: { relatedIds } }, | ||
| ); | ||
| await models.Tags.updateOne( | ||
| { _id: parentTag._id }, | ||
| { | ||
| $set: { | ||
| relatedIds: [ | ||
| ...new Set([...relatedIds, ...(parentTag.relatedIds || [])]), | ||
| ], | ||
| }, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-atomic $set can clobber concurrent updates; use $addToSet/$each for safe deduped merges.
Reading parentTag then $set with a computed union risks lost updates under concurrency.
Apply:
- const relatedIds: string[] = [tag._id, ...(tag.relatedIds || [])];
-
- await models.Tags.updateOne(
- { _id: parentTag._id },
- {
- $set: {
- relatedIds: [
- ...new Set([...relatedIds, ...(parentTag.relatedIds || [])]),
- ],
- },
- },
- );
+ const toAdd = [tag._id, ...(tag.relatedIds || [])];
+ await models.Tags.updateOne(
+ { _id: parentTag._id },
+ { $addToSet: { relatedIds: { $each: toAdd } } },
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const relatedIds: string[] = [tag._id, ...(tag.relatedIds || [])]; | |
| await models.Tags.updateOne( | |
| { _id: parentTag._id }, | |
| { $set: { relatedIds } }, | |
| ); | |
| await models.Tags.updateOne( | |
| { _id: parentTag._id }, | |
| { | |
| $set: { | |
| relatedIds: [ | |
| ...new Set([...relatedIds, ...(parentTag.relatedIds || [])]), | |
| ], | |
| }, | |
| }, | |
| ); | |
| const toAdd = [tag._id, ...(tag.relatedIds || [])]; | |
| await models.Tags.updateOne( | |
| { _id: parentTag._id }, | |
| { $addToSet: { relatedIds: { $each: toAdd } } }, | |
| ); |
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/utils.ts around lines 16 to 27, the code
reads parentTag then uses $set with a computed union which can clobber
concurrent updates; replace the $set union with a single atomic $addToSet update
so MongoDB dedupes safely. Build an array of items to add (e.g. const items =
[tag._id, ...(tag.relatedIds || [])].filter(Boolean)) and call
models.Tags.updateOne({ _id: parentTag._id }, { $addToSet: { relatedIds: {
$each: items } } }); ensuring you no longer rely on parentTag.relatedIds for the
merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/core-api/src/commands/migrateContentType.ts (1)
56-65: Errors exit with code 0 and client is closed twice — use try/catch/finally and set exitCodeCurrently, on failure you log the error, close the client, then still log “finished” and exit(0). This masks failures and duplicates close.
Apply this diff:
- } catch (e) { - console.log(`Error occurred: ${e.message}`); - await client.close(); - } - - console.log(`Process finished at: ${new Date().toISOString()}`); - - await client.close(); - process.exit(); + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + console.error(`Error occurred: ${message}`); + process.exitCode = 1; + } finally { + console.log(`Process finished at: ${new Date().toISOString()}`); + await client.close(); + process.exit(process.exitCode ?? 0); + }
♻️ Duplicate comments (3)
backend/core-api/src/modules/tags/db/models/Tags.ts (3)
88-95: Return the removed tag (interface says ITagDocument); confirm orphaning semantics and relatedIds cleanupReturn the deleted document, not DeleteResult. Also, per PR summary you intend to orphan children by unsetting parentId—confirm removeRelatedTagIds removes this tag and its descendants from all ancestors to avoid stale relatedIds.
- return models.Tags.deleteOne({ _id }); + await models.Tags.deleteOne({ _id }); + return tag;Also applies to: 97-100
24-31: Harden parent validation: self-parent, not-found, and cycle preventionGuard against setting parentId to self, distinguish “not found” vs “not a group”, and prevent cycles (parent is a descendant).
- if (parentId) { - const parentTag = await models.Tags.findOne({ _id: parentId }); - - if (!parentTag?.isGroup) { - throw new Error('Parent tag must be a group'); - } - } + if (parentId) { + if (_id && parentId === _id) { + throw new Error('A tag cannot be its own parent'); + } + const parentTag = await models.Tags.findOne({ _id: parentId }); + if (!parentTag) { + throw new Error('Parent tag not found'); + } + if (!parentTag.isGroup) { + throw new Error('Parent tag must be a group'); + } + if (_id && parentTag.relatedIds?.includes(_id)) { + throw new Error('Cannot set a tag’s parent to its descendant'); + } + }
69-83: Handle parent moves: remove from old ancestry before adding to newWhen parentId changes, you need to remove from the old parent chain, then add to the new one. Also, passing the updated doc to setRelatedTagIds is correct.
- const updated = await models.Tags.findOneAndUpdate({ _id }, doc, { - new: true, - }); - - if (updated) { - await setRelatedTagIds(models, updated); - } + const updated = await models.Tags.findOneAndUpdate({ _id }, doc, { new: true }); + if (updated) { + const parentChanged = + Object.prototype.hasOwnProperty.call(doc, 'parentId') && + String(tag.parentId || '') !== String(updated.parentId || ''); + if (parentChanged) { + // Remove from old ancestry chain + await removeRelatedTagIds(models, tag); + } + // Propagate to new ancestry chain (no-op if no parent) + await setRelatedTagIds(models, updated); + }
🧹 Nitpick comments (2)
backend/core-api/src/commands/migrateContentType.ts (1)
33-37: Tags excluded from this migration — verify handoff and short-circuit when emptyConfirm that all tag-related migrations are fully handled by migrateTags.ts and that no consumers still expect tags to be processed here. Optionally, exit early if COLLECTIONS is empty to avoid a no-op run after connecting.
backend/core-api/src/modules/tags/db/models/Tags.ts (1)
60-66: Consider wrapping create + ancestry updates in a transactionIf consistency across multiple docs matters, run create + setRelatedTagIds in a MongoDB transaction to avoid partial ancestry updates on failures.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
backend/core-api/src/commands/migrateContentType.ts(2 hunks)backend/core-api/src/commands/migrateTags.ts(1 hunks)backend/core-api/src/modules/tags/db/models/Tags.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/core-api/src/commands/migrateTags.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx}: Use functional and declarative programming patterns; avoid classes.
Use TypeScript for all code; prefer interfaces over types.
Avoid enums; use maps instead.
Files:
backend/core-api/src/commands/migrateContentType.tsbackend/core-api/src/modules/tags/db/models/Tags.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Avoid console logs.
Always use absolute paths when importing.
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
Use absolute path when import.
Files:
backend/core-api/src/commands/migrateContentType.tsbackend/core-api/src/modules/tags/db/models/Tags.ts
🧬 Code graph analysis (2)
backend/core-api/src/commands/migrateContentType.ts (1)
backend/erxes-api-shared/src/utils/elasticsearch/utils.ts (1)
client(9-11)
backend/core-api/src/modules/tags/db/models/Tags.ts (2)
backend/erxes-api-shared/src/core-types/modules/tags/tag.ts (1)
ITag(3-9)backend/core-api/src/modules/tags/utils.ts (1)
setRelatedTagIds(5-34)
🔇 Additional comments (1)
backend/core-api/src/modules/tags/db/models/Tags.ts (1)
1-2: Absolute imports and utils wiring look goodPaths follow the repo’s absolute-import guideline; utils are correctly referenced.
| public static async validate(_id: string | null, doc: ITag) { | ||
| const { name, parentId, isGroup } = doc; | ||
|
|
||
| const tag = await models.Tags.findOne({ name }); | ||
|
|
||
| if (tag && tag._id !== _id) { | ||
| throw new Error('There is already a tag with this name'); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
❓ Verification inconclusive
Fix duplicate-name check: exclude self by _id and (optionally) scope by parentId
Current comparison uses ObjectId !== string, which will always be true and will falsely block updates. Also consider scoping uniqueness by parentId if that’s the intended rule.
Apply:
- const tag = await models.Tags.findOne({ name });
-
- if (tag && tag._id !== _id) {
- throw new Error('There is already a tag with this name');
- }
+ const nameQuery: any = { name };
+ // Scope uniqueness to parent context; adjust if global uniqueness is desired
+ if (parentId) {
+ nameQuery.parentId = parentId;
+ } else {
+ nameQuery.$or = [
+ { parentId: { $exists: false } },
+ { parentId: '' },
+ { parentId: null },
+ ];
+ }
+ if (_id) {
+ nameQuery._id = { $ne: _id };
+ }
+ const dup = await models.Tags.findOne(nameQuery);
+ if (dup) {
+ throw new Error('There is already a tag with this name');
+ }Fix duplicate-name check: exclude current tag by _id and scope by parentId
Replace the findOne({ name }) + if (tag._id !== _id) check—which always fails because tag._id is an ObjectId and _id is a string—with a single query that excludes the incoming _id and (if required) limits to the same parentId. For example:
const query: any = { name };
if (parentId) {
query.parentId = parentId;
} else {
query.$or = [
{ parentId: { $exists: false } },
{ parentId: null },
{ parentId: '' },
];
}
if (_id) {
query._id = { $ne: new mongoose.Types.ObjectId(_id) };
}
if (await models.Tags.findOne(query)) {
throw new Error('There is already a tag with this name');
}This ensures updates to an existing tag don’t conflict with itself, and enforces uniqueness within the intended scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Changes requested ❌
Reviewed f83ea3c in 1 minute and 53 seconds. Click for details.
- Reviewed
208lines of code in7files - Skipped
0files when reviewing. - Skipped posting
4draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. backend/core-api/src/commands/migrateContentType.ts:37
- Draft comment:
Ensure including the 'tags' collection for migration is intentional even though the comment previously noted that tags no longer include a contentType field. Verify that updating the 'type' field here is expected. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% The comment violates several rules: 1) It starts with "Ensure" and "Verify", which is explicitly discouraged 2) It's asking the author to confirm their intention rather than pointing out a clear issue 3) If tags truly don't have a type field, the script would fail immediately, making this a self-checking issue 4) The comment doesn't suggest any specific code changes. The comment might be highlighting a real inconsistency between the old comment and new code. The script could fail at runtime if tags really don't have a type field. Even if there is an inconsistency, the comment doesn't propose any specific fix, and runtime failures would make any issues immediately apparent during testing. The author likely knows what they're doing by explicitly adding it back. Delete the comment as it violates multiple comment rules by asking for verification without proposing specific changes, and any real issues would be caught during testing.
2. backend/core-api/src/commands/migrateTags.ts:30
- Draft comment:
Consider using null instead of an empty string for 'parentId' when flattening tags, as null more clearly indicates the absence of a parent. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 20% vs. threshold = 50% While the suggestion to use null over empty string has some merit from a semantic perspective, this is a migration script that runs once to transform data. The choice between null vs empty string is largely a matter of preference here, and both would work equally well to indicate "no parent". The comment doesn't point out a bug or significant code quality issue. The comment could be correct from a best practices standpoint - null is often considered more semantically appropriate for representing absence of a value in databases. However, this is a one-time migration script where the semantic difference between null and empty string is not impactful enough to warrant a change. Both approaches would achieve the same result. Delete the comment as it suggests a minor stylistic change that doesn't meaningfully impact the functionality of this migration script.
3. backend/core-api/src/modules/tags/graphql/queries.ts:24
- Draft comment:
Constructing a regex directly from searchValue may expose you to regex injection or performance issues. Consider escaping special regex characters. - Reason this comment was not posted:
Comment was on unchanged code.
4. backend/core-api/src/modules/tags/db/models/Tags.ts:46
- Draft comment:
There's a trailing space in the error message string: "Nested group is not allowed 2 ". Please remove the trailing space for consistency. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% While technically correct, this is an extremely minor formatting issue that doesn't affect functionality. Looking at the rules, we should not make comments that are obvious or unimportant. A trailing space in an error message is very minor and could be caught by a linter if it was important to the team. Maybe consistent error message formatting is important for this team's standards? Maybe there's a style guide that explicitly calls this out? Even if there are style guidelines about spaces, this is too minor to warrant a PR comment. This kind of feedback should be handled by automated tools like linters or formatters. Delete this comment as it points out an extremely minor formatting issue that doesn't meaningfully impact the code.
Workflow ID: wflow_UmZk1hwXyDQsDeOS
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
|
|
||
| return tag; | ||
| } | ||
| if (tag?.name === name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The uniqueness check for tag names does not exclude the current tag when updating, causing false duplicate errors. Adjust the check to compare tag._id with _id.
| if (tag?.name === name) { | |
| if (tag?.name === name && tag._id.toString() !== _id) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/core-api/src/modules/tags/graphql/queries.ts (1)
100-102: Duplicate: sanitize regex in count resolverApply the same escaping and bound length to prevent ReDoS.
- if (searchValue) { - selector.name = new RegExp(`.*${searchValue}.*`, 'i'); - } + if (searchValue) { + const ESCAPE_RE = /[.*+?^${}()|[\]\\]/g; + const safe = String(searchValue).slice(0, 100).replace(ESCAPE_RE, '\\$&'); + selector.name = { $regex: safe, $options: 'i' }; + }
♻️ Duplicate comments (7)
backend/core-api/src/modules/tags/graphql/queries.ts (3)
9-9: Use correct FilterQuery generic (document type, not params)Type the Mongo filter against the document shape.
-import { FilterQuery } from 'mongoose'; +import { FilterQuery } from 'mongoose'; +import { ITagDocument } from 'erxes-api-shared/core-types'; @@ - const filter: FilterQuery<ITagFilterQueryParams> = { ...commonQuerySelector }; + const filter: FilterQuery<ITagDocument> = { ...commonQuerySelector };
31-33: Allow explicit isGroup=false filteringGuard for explicit boolean; current code skips false.
- if (isGroup) { - filter.isGroup = isGroup; - } + if (typeof isGroup === 'boolean') { + filter.isGroup = isGroup; + }
23-25: Sanitize user input in regex to prevent ReDoSEscape and bound
searchValue; prefer$regexobject.- if (searchValue) { - filter.name = new RegExp(`.*${searchValue}.*`, 'i'); - } + if (searchValue) { + const ESCAPE_RE = /[.*+?^${}()|[\]\\]/g; + const safe = String(searchValue).slice(0, 100).replace(ESCAPE_RE, '\\$&'); + filter.name = { $regex: safe, $options: 'i' }; + }backend/core-api/src/modules/tags/db/definitions/tags.ts (1)
8-8: Don’t enforce global unique on name; scope uniquenessA field-level
unique: truemakes names globally unique across tenants/parents. Scope to (name, parentId, type) or include tenant key if multi-tenant.- name: { type: String, label: 'Name', unique: true }, + name: { type: String, label: 'Name' },Add a compound unique index (case-insensitive). If a standalone unique exists in prod, we must drop it in a migration before creating this one—I can draft it.
- tagSchema.index({ _id: 1, name: 1, parentId: 1, type: 1 }); + // Query and uniqueness indexes + // Uniqueness within scope (add organizationId/workspaceId if multi-tenant) + tagSchema.index({ name: 1, parentId: 1, type: 1 }, { unique: true, collation: { locale: 'en', strength: 2 } });backend/core-api/src/modules/tags/db/models/Tags.ts (2)
84-105: Handle parent moves correctly: remove stale ancestry only when parentId changesCurrent code removes relatedIds based on presence of children, not on parent change, which leaves stale ancestry when moving a leaf and does extra work when editing unrelated fields.
Apply:
public static async updateTag(_id: string, doc: ITag) { await this.validate(_id, doc); - const tag = await models.Tags.getTag(_id); - - const childTags = await models.Tags.find({ parentId: tag._id }); - - if (childTags.length) { - await removeRelatedTagIds(models, tag); - } + const prev = await models.Tags.getTag(_id); + const parentChanged = + Object.prototype.hasOwnProperty.call(doc, 'parentId') && + String(prev.parentId || '') !== String(doc.parentId || ''); + if (parentChanged) { + await removeRelatedTagIds(models, prev); + } - const updated = await models.Tags.findOneAndUpdate( - { _id: tag._id }, + const updated = await models.Tags.findOneAndUpdate( + { _id }, doc, { new: true, }, ); if (updated) { await setRelatedTagIds(models, updated); } return updated; }
113-121: Child tags are orphaned on delete — confirm intentional and document behaviorUnsetting parentId on children leaves them top-level. If intentional (per PR), ensure callers and UI handle orphaned child tags; otherwise consider cascade delete or reassignment strategy.
Would you like a follow-up to add an optional cascading delete flag or a “reassign to parent’s parent” strategy?
backend/core-api/src/modules/tags/graphql/schemas.ts (1)
40-41: Fix tagsQueryCount signature: remove “type”, add isGroup (and optional parentId/instanceId)Schema still exposes the removed argument.
- tagsQueryCount(type: String, searchValue: String): Int + tagsQueryCount(searchValue: String, isGroup: Boolean, parentId: String, instanceId: String): IntIf you prefer strict parity, accept
${queryParams}-like args for count.
🧹 Nitpick comments (3)
backend/core-api/src/modules/tags/graphql/queries.ts (1)
7-8: Avoid shadowingids; dedupe collected IDsThe inner
idsshadows the param and can confuse maintainers; also dedupe to prevent inflated $in arrays.- const { searchValue, parentId, ids, excludeIds, isGroup, type } = params; + const { searchValue, parentId, ids, excludeIds, isGroup, type } = params; @@ - let ids = [parentId, ...parentTag]; + let collectedIds = [parentId, ...parentTag]; @@ - ids = [...ids, ...childTag]; + collectedIds = [...collectedIds, ...childTag]; @@ - await getChildTags(parentTag); + await getChildTags(parentTag); + collectedIds = Array.from(new Set(collectedIds)); @@ - filter._id = { $in: ids }; + filter._id = { $in: collectedIds };Also applies to: 38-54
backend/core-api/src/modules/tags/graphql/schemas.ts (2)
51-55: Optional: return a typed payload for tagsTag instead of JSONTyped payload improves client ergonomics and schema discoverability.
export const mutations = ` - tagsAdd(name: String!, ${mutationParams}): Tag - tagsEdit(_id: String!, name: String, ${mutationParams}): Tag - tagsTag(targetIds: [String!]!, tagIds: [String!]!): JSON + tagsAdd(name: String!, ${mutationParams}): Tag + tagsEdit(_id: String!, name: String, ${mutationParams}): Tag + tagsTag(targetIds: [String!]!, tagIds: [String!]!): TagTagResult tagsRemove(_id: String!): JSON `; + +export const types = ` + type TagTagResult { + modifiedCount: Int! + skippedCount: Int! + } + ${types} +`;Ensure resolver returns this shape.
3-21: Minor: consider non-nullables where guaranteed (future cleanup)e.g., make
_idandnamenon-nullable if guaranteed by DB. Low priority; align after migrations stabilize.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
backend/core-api/src/commands/migrateContentType.ts(1 hunks)backend/core-api/src/commands/migrateTags.ts(1 hunks)backend/core-api/src/modules/tags/db/definitions/tags.ts(1 hunks)backend/core-api/src/modules/tags/db/models/Tags.ts(3 hunks)backend/core-api/src/modules/tags/graphql/queries.ts(2 hunks)backend/core-api/src/modules/tags/graphql/schemas.ts(2 hunks)backend/erxes-api-shared/src/core-types/modules/tags/tag.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- backend/core-api/src/commands/migrateContentType.ts
- backend/core-api/src/commands/migrateTags.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx}: Use functional and declarative programming patterns; avoid classes.
Use TypeScript for all code; prefer interfaces over types.
Avoid enums; use maps instead.
Files:
backend/erxes-api-shared/src/core-types/modules/tags/tag.tsbackend/core-api/src/modules/tags/graphql/queries.tsbackend/core-api/src/modules/tags/db/definitions/tags.tsbackend/core-api/src/modules/tags/db/models/Tags.tsbackend/core-api/src/modules/tags/graphql/schemas.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Avoid console logs.
Always use absolute paths when importing.
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
Use absolute path when import.
Files:
backend/erxes-api-shared/src/core-types/modules/tags/tag.tsbackend/core-api/src/modules/tags/graphql/queries.tsbackend/core-api/src/modules/tags/db/definitions/tags.tsbackend/core-api/src/modules/tags/db/models/Tags.tsbackend/core-api/src/modules/tags/graphql/schemas.ts
🧬 Code graph analysis (3)
backend/core-api/src/modules/tags/graphql/queries.ts (1)
backend/core-api/src/modules/tags/@types/tag.ts (1)
ITagFilterQueryParams(6-15)
backend/core-api/src/modules/tags/db/models/Tags.ts (2)
backend/erxes-api-shared/src/core-types/modules/tags/tag.ts (1)
ITag(3-10)backend/core-api/src/modules/tags/utils.ts (2)
setRelatedTagIds(5-34)removeRelatedTagIds(37-70)
backend/core-api/src/modules/tags/graphql/schemas.ts (1)
backend/erxes-api-shared/src/utils/apollo/constants.ts (1)
GQL_CURSOR_PARAM_DEFS(1-9)
🪛 ast-grep (0.38.6)
backend/core-api/src/modules/tags/graphql/queries.ts
[warning] 23-23: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(.*${searchValue}.*, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (5)
backend/erxes-api-shared/src/core-types/modules/tags/tag.ts (1)
7-9: Confirm optionality of new fields vs DB schema
typeandparentIdare optional here, while DB lookups and filters frequently require them. Mismatch can surface as partial documents at boundaries (GraphQL/DB). Please verify intended nullability across schema/validators.backend/core-api/src/modules/tags/graphql/queries.ts (2)
11-21: Make contentType construction consistent (instanceId handling)
tags()augmentstypewithinstanceId;tagsQueryCount()setsselector.type = typedirectly. This can skew count vs list. Align both (or accept an explicitinstanceIdarg in count).Also applies to: 96-99
112-112: LGTM: use model helper for tag detailSwitch to
getTag(_id)improves reuse/validation.backend/core-api/src/modules/tags/db/models/Tags.ts (2)
64-72: LGTM: getTag error handling is appropriateClear 404-style error and typed return.
74-82: LGTM: create flow + relationship propagationValidation pre-check and ancestry propagation via setRelatedTagIds are correct; helper already no-ops without a parent.
| parentId: { type: String, label: 'Parent' }, | ||
| relatedIds: { type: [String], label: 'Children tag ids' }, | ||
| isGroup: { type: Boolean, label: 'Is group', default: false }, | ||
| type: { type: String, label: 'Content type' }, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix ineffective indexes; add ones matching query patterns
The current compound index starts with _id, making it unusable for filtering by parentId, type, or isGroup. Reinstate targeted indexes for hot paths (tree traversal, type scoping, group filtering).
- tagSchema.index({ _id: 1, name: 1, parentId: 1, type: 1 });
+ // Efficient lookups by hierarchy and content type
+ tagSchema.index({ parentId: 1 });
+ tagSchema.index({ type: 1, isGroup: 1 });
+ // Optional helper for search-by-name within a parent/type scope
+ tagSchema.index({ parentId: 1, type: 1, name: 1 });Also applies to: 21-21
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/db/definitions/tags.ts around lines 10-14
(and also apply same change at line 21), the existing compound index that starts
with _id is ineffective for queries filtering by parentId, type, or isGroup;
remove or stop relying on that index and add targeted indexes that match hot
query patterns: create a single-field index on parentId for tree traversal, a
compound index { parentId: 1, type: 1 } for scoped child lookups by content
type, a compound index { type: 1, isGroup: 1 } for fast group filtering within a
type, and add a multikey index on relatedIds if you query by child id; ensure
these indexes are defined in the schema/indexes section and avoid putting _id as
the leading key in compound indexes used for filtering.
| public static async validate(_id: string | null, doc: ITag) { | ||
| const { name, parentId, isGroup } = doc; | ||
|
|
||
| if (!tag) { | ||
| throw new Error('Tag not found'); | ||
| } | ||
| const tag = await models.Tags.findOne({ | ||
| $or: [{ _id }, { name }], | ||
| }); | ||
|
|
||
| return tag; | ||
| } | ||
| if (tag?.name === name) { | ||
| throw new Error(`A tag named ${name} already exists`); | ||
| } | ||
|
|
||
| /** | ||
| * Create a tag | ||
| */ | ||
| public static async createTag(doc: ITag) { | ||
| const isUnique = await this.validateUniqueness(null, doc.name, doc.type); | ||
| if (tag?.isGroup && isGroup) { | ||
| throw new Error('Nested group is not allowed 1'); | ||
| } | ||
|
|
||
| if (!isUnique) { | ||
| throw new Error('Tag duplicated'); | ||
| if (_id === parentId) { | ||
| throw new Error('Group cannot be itself'); | ||
| } | ||
|
|
||
| const parentTag = await this.getParentTag(doc); | ||
| if (parentId) { | ||
| const parentTag = await models.Tags.findOne({ _id: parentId }); | ||
|
|
||
| // Generatingg order | ||
| const order = await this.generateOrder(parentTag, doc); | ||
| if (!parentTag) { | ||
| throw new Error('Group not found'); | ||
| } | ||
|
|
||
| const tag = await models.Tags.create({ | ||
| ...doc, | ||
| order, | ||
| createdAt: new Date(), | ||
| }); | ||
| if (!parentTag.isGroup) { | ||
| throw new Error('Parent tag must be a group'); | ||
| } | ||
|
|
||
| await setRelatedTagIds(models, tag); | ||
| if ((isGroup || tag?.isGroup) && parentTag?.isGroup) { | ||
| throw new Error('Nested group is not allowed 2 '); | ||
| } | ||
| } | ||
|
|
||
| return tag; | ||
| } | ||
| if (tag) { | ||
| const parentTag = await models.Tags.findOne({ _id: tag.parentId }); | ||
| const childTags = await models.Tags.find({ parentId: tag._id }); | ||
|
|
||
| /** | ||
| * Update Tag | ||
| */ | ||
| public static async updateTag(_id: string, doc: ITag) { | ||
| const isUnique = await this.validateUniqueness( | ||
| { _id }, | ||
| doc.name, | ||
| doc.type, | ||
| ); | ||
| if (parentTag?.isGroup && isGroup) { | ||
| throw new Error('Nested group is not allowed 3'); | ||
| } | ||
|
|
||
| if (!isUnique) { | ||
| throw new Error('Tag duplicated'); | ||
| if (!isGroup && childTags.length) { | ||
| throw new Error('Group has tags'); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix validation: duplicate-name check, parent/group rules, self-parent, and cycle prevention
Current logic mis-detects duplicates, conflates parent/group constraints, and can block benign edits (e.g., when isGroup is omitted). Consolidate validation using the effective next state, exclude self in duplicate checks, improve errors, and prevent cycles and self-parenting.
Apply:
- public static async validate(_id: string | null, doc: ITag) {
- const { name, parentId, isGroup } = doc;
-
- const tag = await models.Tags.findOne({
- $or: [{ _id }, { name }],
- });
-
- if (tag?.name === name) {
- throw new Error(`A tag named ${name} already exists`);
- }
-
- if (tag?.isGroup && isGroup) {
- throw new Error('Nested group is not allowed 1');
- }
-
- if (_id === parentId) {
- throw new Error('Group cannot be itself');
- }
-
- if (parentId) {
- const parentTag = await models.Tags.findOne({ _id: parentId });
-
- if (!parentTag) {
- throw new Error('Group not found');
- }
-
- if (!parentTag.isGroup) {
- throw new Error('Parent tag must be a group');
- }
-
- if ((isGroup || tag?.isGroup) && parentTag?.isGroup) {
- throw new Error('Nested group is not allowed 2 ');
- }
- }
-
- if (tag) {
- const parentTag = await models.Tags.findOne({ _id: tag.parentId });
- const childTags = await models.Tags.find({ parentId: tag._id });
-
- if (parentTag?.isGroup && isGroup) {
- throw new Error('Nested group is not allowed 3');
- }
-
- if (!isGroup && childTags.length) {
- throw new Error('Group has tags');
- }
- }
- }
+ public static async validate(_id: string | null, doc: ITag) {
+ const { name, parentId, isGroup } = doc;
+
+ // 1) Unique name (exclude self on update)
+ const dup = await models.Tags.findOne({
+ name,
+ ...(!!_id ? { _id: { $ne: _id } } : {}),
+ });
+ if (dup) {
+ throw new Error(`A tag named ${name} already exists`);
+ }
+
+ // 2) Load existing on update and compute effective next state
+ const existing = _id ? await models.Tags.findOne({ _id }) : null;
+ const hasIsGroup = Object.prototype.hasOwnProperty.call(doc, 'isGroup');
+ const hasParentId = Object.prototype.hasOwnProperty.call(doc, 'parentId');
+ const nextIsGroup = hasIsGroup ? !!isGroup : !!existing?.isGroup;
+ const nextParentId = hasParentId ? parentId : existing?.parentId;
+
+ // 3) Self-parent
+ if (_id && nextParentId && String(_id) === String(nextParentId)) {
+ throw new Error('A tag cannot be its own parent');
+ }
+
+ // 4) Parent existence + type + cycle
+ if (nextParentId) {
+ const parentTag = await models.Tags.findOne({ _id: nextParentId });
+ if (!parentTag) {
+ throw new Error('Parent tag not found');
+ }
+ if (!parentTag.isGroup) {
+ throw new Error('Parent tag must be a group');
+ }
+ // Prevent cycles: parent cannot be a descendant of current
+ if (_id && (parentTag.relatedIds || []).includes(_id)) {
+ throw new Error('Cannot set a tag’s parent to its descendant');
+ }
+ }
+
+ // 5) Group cannot have a parent
+ if (nextIsGroup && nextParentId) {
+ throw new Error('Group tag cannot have a parent');
+ }
+
+ if (existing) {
+ // 6) Disallow converting a nested tag into a group
+ if (!existing.isGroup && nextIsGroup && !!existing.parentId) {
+ throw new Error('Cannot convert a nested tag into a group');
+ }
+ // 7) Prevent demoting a group that already has children
+ if (existing.isGroup && !nextIsGroup) {
+ const childCount = await models.Tags.countDocuments({ parentId: _id });
+ if (childCount > 0) {
+ throw new Error(
+ 'Cannot convert a group with children into a non-group',
+ );
+ }
+ }
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public static async validate(_id: string | null, doc: ITag) { | |
| const { name, parentId, isGroup } = doc; | |
| if (!tag) { | |
| throw new Error('Tag not found'); | |
| } | |
| const tag = await models.Tags.findOne({ | |
| $or: [{ _id }, { name }], | |
| }); | |
| return tag; | |
| } | |
| if (tag?.name === name) { | |
| throw new Error(`A tag named ${name} already exists`); | |
| } | |
| /** | |
| * Create a tag | |
| */ | |
| public static async createTag(doc: ITag) { | |
| const isUnique = await this.validateUniqueness(null, doc.name, doc.type); | |
| if (tag?.isGroup && isGroup) { | |
| throw new Error('Nested group is not allowed 1'); | |
| } | |
| if (!isUnique) { | |
| throw new Error('Tag duplicated'); | |
| if (_id === parentId) { | |
| throw new Error('Group cannot be itself'); | |
| } | |
| const parentTag = await this.getParentTag(doc); | |
| if (parentId) { | |
| const parentTag = await models.Tags.findOne({ _id: parentId }); | |
| // Generatingg order | |
| const order = await this.generateOrder(parentTag, doc); | |
| if (!parentTag) { | |
| throw new Error('Group not found'); | |
| } | |
| const tag = await models.Tags.create({ | |
| ...doc, | |
| order, | |
| createdAt: new Date(), | |
| }); | |
| if (!parentTag.isGroup) { | |
| throw new Error('Parent tag must be a group'); | |
| } | |
| await setRelatedTagIds(models, tag); | |
| if ((isGroup || tag?.isGroup) && parentTag?.isGroup) { | |
| throw new Error('Nested group is not allowed 2 '); | |
| } | |
| } | |
| return tag; | |
| } | |
| if (tag) { | |
| const parentTag = await models.Tags.findOne({ _id: tag.parentId }); | |
| const childTags = await models.Tags.find({ parentId: tag._id }); | |
| /** | |
| * Update Tag | |
| */ | |
| public static async updateTag(_id: string, doc: ITag) { | |
| const isUnique = await this.validateUniqueness( | |
| { _id }, | |
| doc.name, | |
| doc.type, | |
| ); | |
| if (parentTag?.isGroup && isGroup) { | |
| throw new Error('Nested group is not allowed 3'); | |
| } | |
| if (!isUnique) { | |
| throw new Error('Tag duplicated'); | |
| if (!isGroup && childTags.length) { | |
| throw new Error('Group has tags'); | |
| } | |
| } | |
| } | |
| public static async validate(_id: string | null, doc: ITag) { | |
| const { name, parentId, isGroup } = doc; | |
| // 1) Unique name (exclude self on update) | |
| const dup = await models.Tags.findOne({ | |
| name, | |
| ...(!!_id ? { _id: { $ne: _id } } : {}), | |
| }); | |
| if (dup) { | |
| throw new Error(`A tag named ${name} already exists`); | |
| } | |
| // 2) Load existing on update and compute effective next state | |
| const existing = _id ? await models.Tags.findOne({ _id }) : null; | |
| const hasIsGroup = Object.prototype.hasOwnProperty.call(doc, 'isGroup'); | |
| const hasParentId = Object.prototype.hasOwnProperty.call(doc, 'parentId'); | |
| const nextIsGroup = hasIsGroup ? !!isGroup : !!existing?.isGroup; | |
| const nextParentId = hasParentId ? parentId : existing?.parentId; | |
| // 3) Self-parent guard | |
| if (_id && nextParentId && String(_id) === String(nextParentId)) { | |
| throw new Error('A tag cannot be its own parent'); | |
| } | |
| // 4) Parent existence, type, and cycle prevention | |
| if (nextParentId) { | |
| const parentTag = await models.Tags.findOne({ _id: nextParentId }); | |
| if (!parentTag) { | |
| throw new Error('Parent tag not found'); | |
| } | |
| if (!parentTag.isGroup) { | |
| throw new Error('Parent tag must be a group'); | |
| } | |
| // Prevent setting a tag’s parent to its own descendant | |
| if (_id && (parentTag.relatedIds || []).includes(_id)) { | |
| throw new Error('Cannot set a tag’s parent to its descendant'); | |
| } | |
| } | |
| // 5) Disallow any parent on a group | |
| if (nextIsGroup && nextParentId) { | |
| throw new Error('Group tag cannot have a parent'); | |
| } | |
| if (existing) { | |
| // 6) Prevent converting a nested tag into a group | |
| if (!existing.isGroup && nextIsGroup && !!existing.parentId) { | |
| throw new Error('Cannot convert a nested tag into a group'); | |
| } | |
| // 7) Prevent demoting a group that already has children | |
| if (existing.isGroup && !nextIsGroup) { | |
| const childCount = await models.Tags.countDocuments({ parentId: _id }); | |
| if (childCount > 0) { | |
| throw new Error( | |
| 'Cannot convert a group with children into a non-group', | |
| ); | |
| } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/db/models/Tags.ts around lines 15 to 62,
compute the effective next state (treat undefined isGroup as existing
tag.isGroup when editing) and then: 1) check for duplicate name excluding the
current _id (findOne({ name, _id: { $ne: _id } })); 2) ensure parentId !== _id
and walk the parent chain to prevent cycles (follow parentId up until null and
throw if any ancestor === _id); 3) verify parent exists and parent.isGroup ===
true; 4) forbid nested groups by disallowing a tag with isGroup === true to have
a parent that isGroup === true; 5) if changing/setting isGroup to false, ensure
there are no child tags (childTags.length === 0) before allowing the change; and
improve throw messages to be specific for each failure.
| isGroup: Boolean | ||
| type: String | ||
| createdAt: Date |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Remove or deprecate legacy “type” field from Tag
PR objective states removal of legacy “type”; keeping it in the schema risks drift. Either remove it or mark deprecated pending migration.
Option A (remove):
type Tag @key(fields: "_id") @cacheControl(maxAge: 3) {
_id: String
name: String
colorCode: String
parentId: String
relatedIds: [String]
isGroup: Boolean
- type: String
-
createdAt: Date
}Option B (deprecate):
- type: String
+ type: String @deprecated(reason: "Legacy field; scheduled for removal")🏁 Script executed:
#!/bin/bash
# 1. Locate Tag entity/class definitions (TypeORM, Mongoose, etc.)
rg -nP --type=ts 'class\s+Tag\b' -C2
# 2. Search for TagSchema (Mongoose)
rg -nP 'TagSchema' -n
# 3. Find GraphQL schema file declaring Tag (ensure “type” appears only here)
rg -nP 'type\s+Tag' -C3 backend/core-api/src/modules/tags/graphql/schemas.ts
# 4. Search all resolvers/services for accesses to `.type` on Tag objects
rg -nP '\btype\b' -C2 backend/core-api/src/modules/tags
# 5. Grep for GraphQL queries/fragments using “type” on Tag
rg -nP 'Tag\s*\{\s*[^}]*type' -C2Length of output: 10323
Deprecate legacy type field on GraphQL Tag and plan its removal
- In backend/core-api/src/modules/tags/graphql/schemas.ts, add
@deprecated(reason: "Legacy field; scheduled for removal")to thetype: Stringfield. - Coordinate migration by updating usages in graphql/queries.ts and graphql/mutations.ts, then remove it from the Mongoose schema (db/definitions/tags.ts) and TypeScript definitions ( @types/tag.ts ) once clients have migrated.
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/graphql/schemas.ts around lines 10 to 13,
mark the GraphQL field `type: String` as deprecated by adding the @deprecated
directive with reason "Legacy field; scheduled for removal". After adding the
directive, search and update all usages of the `type` field in
graphql/queries.ts and graphql/mutations.ts to use the new/alternate field or
handle its absence (add fallback logic or warnings), and coordinate with clients
about the deprecation. Once clients have migrated, remove the `type` field from
the Mongoose schema at db/definitions/tags.ts and from TypeScript definitions in
@types/tag.ts, ensuring any database migrations or data cleanup are planned
before removal.
| type: String, | ||
| searchValue: String, | ||
| tagIds: [String], | ||
| parentId: String, | ||
| ids: [String], | ||
| excludeIds: Boolean, | ||
| isGroup: Boolean, | ||
| instanceId: String, | ||
| sortField: String, | ||
| ${GQL_CURSOR_PARAM_DEFS} | ||
| `; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Align query params with new model: drop “type”, keep isGroup/instanceId
Retaining type contradicts the PR. Align filters; consider reusing the same param block for tagsQueryCount for parity.
const queryParams = `
- type: String,
searchValue: String,
parentId: String,
ids: [String],
excludeIds: Boolean,
isGroup: Boolean,
instanceId: String,
sortField: String,
${GQL_CURSOR_PARAM_DEFS}
`;🏁 Script executed:
#!/bin/bash
set -e
# Show tagsQuery definition context
rg -n -C5 "tagsQuery" backend/core-api/src/modules/tags/graphql/schemas.ts || true
# Show tagsQueryCount definition context
rg -n -C5 "tagsQueryCount" backend/core-api/src/modules/tags/graphql/schemas.ts || trueLength of output: 677
Drop type from tags query params and sync tagsQueryCount filters
Remove type from the queryParams block and update the tagsQueryCount signature to accept the same filters—searchValue, parentId, ids, excludeIds, isGroup, and instanceId.
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/graphql/schemas.ts around lines 24 to 35,
remove the `type: String,` entry from the `queryParams` block and update the
`tagsQueryCount` field/signature so it accepts the same filters as `tags`
(searchValue, parentId, ids, excludeIds, isGroup, instanceId), ensuring the
parameter names and types match exactly and any references/validators are
updated accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important
Looks good to me! 👍
Reviewed 791ae67 in 1 minute and 8 seconds. Click for details.
- Reviewed
39lines of code in1files - Skipped
0files when reviewing. - Skipped posting
6draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. backend/core-api/src/modules/tags/db/models/Tags.ts:20
- Draft comment:
Using .lean() here improves performance by returning plain objects. Ensure no Mongoose document methods or virtuals are needed later. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
2. backend/core-api/src/modules/tags/db/models/Tags.ts:30
- Draft comment:
Converting _id and parentId to strings ensures proper comparison of ObjectId values. Be cautious as both null values become 'null', which may trigger the error unexpectedly. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
3. backend/core-api/src/modules/tags/db/models/Tags.ts:35
- Draft comment:
Consistent use of .lean() for fetching the parentTag optimizes performance. Confirm that no Mongoose document features (hooks, getters) are required here. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
4. backend/core-api/src/modules/tags/db/models/Tags.ts:52
- Draft comment:
Applying .lean() for both the parentTag and childTags queries is a good pattern for read-only validation. Verify that plain objects suffice for subsequent checks. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
5. backend/core-api/src/modules/tags/db/models/Tags.ts:27
- Draft comment:
Typo: The error message 'Nested group is not allowed 1' seems to include an extra '1'. Consider removing it to improve clarity. - Reason this comment was not posted:
Comment was on unchanged code.
6. backend/core-api/src/modules/tags/db/models/Tags.ts:55
- Draft comment:
The error message "Nested group is not allowed 3" appears to include an extraneous '3'. Please verify if this is intentional or if it should be removed. - Reason this comment was not posted:
Comment was on unchanged code.
Workflow ID: wflow_lHstztgGbkhwMPIi
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Changes requested ❌
Reviewed d972adf in 1 minute and 43 seconds. Click for details.
- Reviewed
286lines of code in10files - Skipped
0files when reviewing. - Skipped posting
4draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. backend/core-api/src/commands/migrateTags.ts:29
- Draft comment:
Intentionally omitting 'objectCount' from $unset now that it's part of the schema. Confirm that this change is desired. - Reason this comment was not posted:
Comment looked like it was already resolved.
2. backend/core-api/src/modules/tags/db/definitions/tags.ts:14
- Draft comment:
Consider adding a default value for 'objectCount' to ensure consistent numeric data, if applicable. - Reason this comment was not posted:
Confidence changes required:50%<= threshold50%None
3. backend/core-api/src/modules/tags/graphql/mutations.ts:44
- Draft comment:
Filtering tags by 'type' and 'isGroup: false' ensures only non‐group tags are processed—good change. Validate that the incoming 'type' value is correctly formatted. - Reason this comment was not posted:
Comment did not seem useful. Confidence is useful =0%<= threshold50%The comment starts with a purely informative statement about the change being good, which is not allowed. The second part of the comment asks the author to validate the 'type' value, which is not specific enough to be useful. It doesn't suggest a specific action or point out a specific issue.
4. backend/core-api/src/modules/tags/utils.ts:59
- Draft comment:
The async keyword on the forEach callback is unnecessary since only synchronous operations are performed. Remove async to clarify intent. - Reason this comment was not posted:
Comment was not on a location in the diff, so it can't be submitted as a review comment.
Workflow ID: wflow_8MMK1HuRHw5wDDbM
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
| }); | ||
| }), | ||
|
|
||
| tag: t.procedure.input(z.any()).mutation(async ({ ctx, input }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid using z.any() for input and remove debug console.log; add stricter validation and remove logging of raw inputs in production.
| }).lean(); | ||
| } | ||
|
|
||
| return response; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider handling unexpected 'action' values in the 'tag' procedure (e.g. add a default case or error throw) to avoid returning an empty response.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
backend/core-api/src/modules/tags/graphql/mutations.ts (1)
76-83: Fix TRPC module routing for core services.customers TRPC router is plural; sending moduleName directly ('customer') will miss the route. Reuse the same mapping used in countDocuments.
- return await sendTRPCMessage({ + const MODULE_NAMES: Record<string, string> = { customer: 'customers' }; + return await sendTRPCMessage({ pluginName, method: 'mutation', - module: moduleName, + module: MODULE_NAMES[moduleName] || moduleName, action: 'tag', input: { tagIds: tags.map((tag) => tag._id), targetIds, - type: moduleName, + type: moduleName, action: 'tagObject', }, });backend/core-api/src/modules/tags/utils.ts (1)
42-42: Fix $in usage: it must be an array.Using a scalar with $in can misbehave.
- const tags = await models.Tags.find({ relatedIds: { $in: tag._id } }); + const tags = await models.Tags.find({ relatedIds: { $in: [tag._id] } });
♻️ Duplicate comments (11)
backend/core-api/src/modules/tags/db/definitions/tags.ts (2)
8-8: Scope name uniqueness; avoid global unique on name.Field-level unique: true enforces cross-tenant/global uniqueness and blocks same-name tags under different parents/types. Use a compound unique index instead (and include tenant scope if applicable).
- name: { type: String, label: 'Name', unique: true }, + name: { type: String, label: 'Name' },Add indexes below (and run a migration to drop any existing unique index on name before creating the new one):
- tagSchema.index({ _id: 1, name: 1, parentId: 1, type: 1 }); + // Uniqueness within parent+type (adjust to include tenant/org scope if multi-tenant) + tagSchema.index({ name: 1, parentId: 1, type: 1 }, { unique: true });
10-13: Reinstate hierarchy and array defaults; index hot fields.
- parentId lost its index; tree queries will slow down.
- relatedIds should default to [] to avoid undefined checks.
- Add targeted indexes that match query patterns.
- parentId: { type: String, label: 'Parent' }, - relatedIds: { type: [String], label: 'Children tag ids' }, + parentId: { type: String, label: 'Parent', index: true }, + relatedIds: { type: [String], label: 'Children tag ids', default: [] },And replace the ineffective compound index starting with _id:
-tagSchema.index({ _id: 1, name: 1, parentId: 1, type: 1 }); +// Efficient lookups +tagSchema.index({ parentId: 1 }); +tagSchema.index({ type: 1, isGroup: 1 }); +tagSchema.index({ parentId: 1, type: 1, name: 1 }); +// Optional: if you query by relatedIds +// tagSchema.index({ relatedIds: 1 });backend/core-api/src/modules/tags/graphql/queries.ts (4)
123-125: Apply the same safe-regex pattern here.- if (searchValue) { - selector.name = new RegExp(`.*${searchValue}.*`, 'i'); - } + if (searchValue) { + const ESCAPE_RE = /[.*+?^${}()|[\]\\]/g; + const safe = String(searchValue).slice(0, 100).replace(ESCAPE_RE, '\\$&'); + selector.name = { $regex: safe, $options: 'i' }; + }
9-9: Fix FilterQuery generic to target the document type.FilterQuery should be parameterized by ITagDocument, not the params type.
-import { FilterQuery } from 'mongoose'; +import { FilterQuery } from 'mongoose'; +import { ITagDocument } from 'erxes-api-shared/core-types'; @@ - const filter: FilterQuery<ITagFilterQueryParams> = { ...commonQuerySelector }; + const filter: FilterQuery<ITagDocument> = { ...commonQuerySelector };
23-25: Escape user input in regex (ReDoS hardening).Constructing RegExp from raw input is unsafe. Use $regex with escaping and a length cap.
- if (searchValue) { - filter.name = new RegExp(`.*${searchValue}.*`, 'i'); - } + if (searchValue) { + const ESCAPE_RE = /[.*+?^${}()|[\]\\]/g; + const safe = String(searchValue).slice(0, 100).replace(ESCAPE_RE, '\\$&'); + filter.name = { $regex: safe, $options: 'i' }; + }
31-33: Allow explicit isGroup=false filtering.Current guard skips false; use a boolean check.
- if (isGroup) { - filter.isGroup = isGroup; - } + if (typeof isGroup === 'boolean') { + filter.isGroup = isGroup; + }backend/core-api/src/modules/tags/utils.ts (1)
17-28: Make parent relatedIds update atomic ($addToSet/$each) to avoid clobbering under concurrency.Current $set with a computed union is racy. Use atomic array operators.
- const relatedIds: string[] = [tag._id, ...(tag.relatedIds || [])]; + const toAdd: string[] = [tag._id, ...(tag.relatedIds || [])].filter( + (id): id is string => Boolean(id), + ); - await models.Tags.updateOne( - { _id: parentTag._id }, - { - $set: { - relatedIds: [ - ...new Set([...relatedIds, ...(parentTag.relatedIds || [])]), - ], - }, - }, - ); + await models.Tags.updateOne( + { _id: parentTag._id }, + { $addToSet: { relatedIds: { $each: toAdd } } }, + );backend/core-api/src/modules/tags/graphql/schemas.ts (4)
41-45: Remove tagsGetTypes and update tagsQueryCount signature to mirror filters.Eliminate legacy entry points and avoid schema drift.
- tagsGetTypes: [JSON] + # tagsGetTypes removed with legacy "type" tags(${queryParams}): TagsListResponse tagDetail(_id: String!): Tag - tagsQueryCount(type: String, searchValue: String): Int + tagsQueryCount( + searchValue: String, + parentId: String, + ids: [String], + excludeIds: Boolean, + isGroup: Boolean, + instanceId: String + ): Int#!/bin/bash # Find any remaining callers of legacy APIs/args rg -nP '\btagsGetTypes\b|\btagsQueryCount\s*\(.*type\s*:' -C2 # Find usages of Tag.type in queries/fragments rg -nP 'type\s*:\s*String|Tag\s*\{[^}]*\btype\b' -C3
10-11: Remove or deprecate legacy Tag.type.PR objective says “remove legacy type”; keep it out of the schema (or deprecate if transitional).
type Tag @key(fields: "_id") @cacheControl(maxAge: 3) { _id: String name: String colorCode: String parentId: String relatedIds: [String] isGroup: Boolean - type: StringIf you must keep it temporarily:
- type: String + type: String @deprecated(reason: "Legacy field; scheduled for removal")
26-34: Align query filters with new model: drop type; keep isGroup/instanceId.Retain added filters but remove legacy “type” from queryParams.
const queryParams = ` - type: String, searchValue: String, parentId: String, ids: [String], excludeIds: Boolean, isGroup: Boolean, instanceId: String, sortField: String, ${GQL_CURSOR_PARAM_DEFS} `;
48-52: Remove legacy 'type' from mutationParams.Inputs should mirror the updated model.
const mutationParams = ` - type: String, colorCode: String, parentId: String, isGroup: Boolean, `;
🧹 Nitpick comments (13)
backend/erxes-api-shared/src/core-types/modules/tags/tag.ts (2)
7-10: Prefer readonly arrays and clarify contentType shape.
- Make relatedIds immutable at the type level and document the expected shape of type ("plugin:module[:instanceId]") to prevent misuse.
export interface ITag { name: string; colorCode?: string; parentId?: string; - relatedIds?: string[]; + relatedIds?: readonly string[]; isGroup?: boolean; - type: string; + type: string; // e.g., "core:customer[:<instanceId>]" objectCount?: number; }
13-16: Mark immutable document fields as readonly.These should never be reassigned once persisted.
-export interface ITagDocument extends ITag, Document { - _id: string; - createdAt: Date; -} +export interface ITagDocument extends ITag, Document { + readonly _id: string; + readonly createdAt: Date; +}frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx (2)
11-12: Tighten variable nullability.ids should not contain nulls; declare as [String!]. excludeIds can be optional with a client default of false.
- $ids: [String] - $excludeIds: Boolean + $ids: [String!] + $excludeIds: Boolean
24-32: Expose isGroup to the client.UI often needs to distinguish groups vs. items. Include isGroup in the selection.
list { _id colorCode name parentId + isGroup totalObjectCount objectCount type }backend/core-api/src/modules/tags/graphql/mutations.ts (1)
50-51: Clarify error message for partial mismatches.If any provided tagId is a group or missing, say so explicitly to aid debugging.
- if (tags.length !== tagIds.length) { - throw new Error('Tag not found.'); + if (tags.length !== tagIds.length) { + throw new Error('Some tags not found or are groups; only non-group tags can be attached.'); }backend/core-api/src/modules/tags/graphql/customResolvers/tag.ts (3)
6-8: Use model helper consistently.Align with tagDetail by using getTag for reference resolution (encapsulates common selectors).
- async __resolveReference({ _id }: { _id: string }, { models }: IContext) { - return models.Tags.findOne({ _id }); - }, + async __resolveReference({ _id }: { _id: string }, { models }: IContext) { + return models.Tags.getTag(_id); + },
15-19: Deduplicate IDs and return 0 instead of undefined.Avoid double-counting and return a numeric value consistently.
- if (tag.relatedIds && tag.relatedIds.length > 0) { - const tagIds = tag.relatedIds.concat(tag._id); - - return countDocuments(subdomain, tag.type, tagIds); - } + if (tag.relatedIds && tag.relatedIds.length > 0) { + const tagIds = Array.from(new Set([...tag.relatedIds, tag._id])); + return countDocuments(subdomain, tag.type, tagIds); + } + return 0;
22-28: Short-circuit for undefined type.If type is missing, return 0 to avoid an unnecessary RPC.
- return countDocuments(subdomain, tag.type, [tag._id]); + if (!tag.type) return 0; + return countDocuments(subdomain, tag.type, [tag._id]);backend/core-api/src/modules/tags/graphql/queries.ts (1)
35-54: Optimize descendant expansion or gate recursion.The N+1 recursive lookups can be heavy on deep trees. Consider $graphLookup or limiting depth, and ensure parentId is indexed (see schema comment).
backend/core-api/src/modules/tags/utils.ts (3)
30-34: Guard against accidental parent cycles.If data ever forms a cycle, recursion will loop. Ensure upstream validation prevents cycles, or add a visited set to break cycles.
59-68: Drop unnecessary async inside forEach.No awaits inside; avoid misleading async usage.
- tags.forEach(async (t) => { + tags.forEach((t) => {
73-99: Remove or use unused parameter subdomain.Either thread subdomain through sendTRPCMessage options/headers or drop it from the signature.
backend/core-api/src/modules/tags/graphql/schemas.ts (1)
55-57: Strongly type tagsTag result instead of JSON.Expose a minimal result payload for introspection/clients.
- tagsTag(targetIds: [String!]!, tagIds: [String!]!): JSON + tagsTag(targetIds: [String!]!, tagIds: [String!]!): TagTagResultAdd this type to the SDL:
type TagTagResult { modifiedCount: Int! skippedCount: Int! }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
backend/core-api/src/commands/migrateTags.ts(1 hunks)backend/core-api/src/modules/contacts/trpc/customer.ts(1 hunks)backend/core-api/src/modules/tags/db/definitions/tags.ts(1 hunks)backend/core-api/src/modules/tags/graphql/customResolvers/tag.ts(1 hunks)backend/core-api/src/modules/tags/graphql/mutations.ts(3 hunks)backend/core-api/src/modules/tags/graphql/queries.ts(3 hunks)backend/core-api/src/modules/tags/graphql/schemas.ts(3 hunks)backend/core-api/src/modules/tags/utils.ts(2 hunks)backend/erxes-api-shared/src/core-types/modules/tags/tag.ts(1 hunks)frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/core-api/src/commands/migrateTags.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx}: Use functional and declarative programming patterns; avoid classes.
Use TypeScript for all code; prefer interfaces over types.
Avoid enums; use maps instead.
Files:
backend/core-api/src/modules/contacts/trpc/customer.tsfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsxbackend/core-api/src/modules/tags/graphql/customResolvers/tag.tsbackend/core-api/src/modules/tags/graphql/mutations.tsbackend/core-api/src/modules/tags/graphql/queries.tsbackend/core-api/src/modules/tags/graphql/schemas.tsbackend/erxes-api-shared/src/core-types/modules/tags/tag.tsbackend/core-api/src/modules/tags/utils.tsbackend/core-api/src/modules/tags/db/definitions/tags.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Avoid console logs.
Always use absolute paths when importing.
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
Use absolute path when import.
Files:
backend/core-api/src/modules/contacts/trpc/customer.tsfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsxbackend/core-api/src/modules/tags/graphql/customResolvers/tag.tsbackend/core-api/src/modules/tags/graphql/mutations.tsbackend/core-api/src/modules/tags/graphql/queries.tsbackend/core-api/src/modules/tags/graphql/schemas.tsbackend/erxes-api-shared/src/core-types/modules/tags/tag.tsbackend/core-api/src/modules/tags/utils.tsbackend/core-api/src/modules/tags/db/definitions/tags.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{tsx,jsx}: Favor named exports for components.
Use declarative JSX.
Use Shadcn UI, Radix, and Tailwind for components and styling.
Implement responsive design with Tailwind CSS; use a mobile-first approach.
**/*.{tsx,jsx}: Structure files: exported component, subcomponents, helpers, static content, types.
Favor named exports for components.
Use declarative JSX.
Use Shadcn UI, Radix, and Tailwind for components and styling.
Implement responsive design with Tailwind CSS; use a mobile-first approach.
Files:
frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx
**/*.tsx
📄 CodeRabbit inference engine (.cursorrules)
**/*.tsx: Use functional components with TypeScript interfaces.
Minimize 'useEffect', and 'setState'.
Wrap client components in Suspense with fallback.
Use dynamic loading for non-critical components.
Limit 'use client': Favor server components and Next.js SSR. Use only for Web API access in small components. Avoid for data fetching or state management.
Files:
frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx
**/*.{webp,tsx,jsx}
📄 CodeRabbit inference engine (.cursorrules)
Optimize images: use WebP format, include size data, implement lazy loading.
Files:
frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx
🧬 Code graph analysis (5)
backend/core-api/src/modules/tags/graphql/customResolvers/tag.ts (2)
backend/erxes-api-shared/src/core-types/modules/tags/tag.ts (1)
ITagDocument(13-16)backend/core-api/src/modules/tags/utils.ts (1)
countDocuments(73-99)
backend/core-api/src/modules/tags/graphql/mutations.ts (2)
backend/core-api/src/modules/tags/graphql/queries.ts (1)
tags(86-104)backend/core-api/src/modules/automations/graphql/resolvers/customResolver/automations.ts (1)
tags(21-23)
backend/core-api/src/modules/tags/graphql/queries.ts (2)
backend/core-api/src/modules/tags/@types/tag.ts (1)
ITagFilterQueryParams(6-15)backend/erxes-api-shared/src/utils/service-discovery.ts (2)
getPlugins(20-25)getPlugin(30-49)
backend/core-api/src/modules/tags/graphql/schemas.ts (3)
backend/core-api/src/modules/contacts/graphql/schemas/company.ts (1)
mutations(123-128)backend/core-api/src/modules/products/graphql/schemas/product.ts (2)
mutations(121-127)mutationParams(99-119)backend/core-api/src/modules/organization/brand/graphql/schema.ts (1)
mutations(43-47)
backend/core-api/src/modules/tags/utils.ts (1)
backend/erxes-api-shared/src/utils/trpc/index.ts (1)
sendTRPCMessage(42-68)
🪛 ast-grep (0.38.6)
backend/core-api/src/modules/tags/graphql/queries.ts
[warning] 23-23: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(.*${searchValue}.*, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: accounting_ui-ci
- GitHub Check: frontline_ui-ci
- GitHub Check: notifications-ci
- GitHub Check: gateway-ci
- GitHub Check: core-ui
- GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (5)
frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx (1)
53-57: GraphQL schema confirms tagsGetTypes is exposed
The field tagsGetTypes is defined in schemas.ts (line 41) and implemented in queries.ts (line 63); no changes needed.backend/core-api/src/modules/tags/graphql/queries.ts (1)
61-70: ResolvertagsGetTypesis still exposed inschemas.ts(line 41) and remains in use; no removal needed.backend/core-api/src/modules/tags/utils.ts (2)
7-15: Early-exit and parent existence checks look good.Prevents unnecessary queries/work when parentId is absent or missing parent.
88-98: Confirm TRPC method; count should likely be query.Method is set to 'mutation' for a read-only count. Verify server expects mutation; otherwise switch to 'query'.
backend/core-api/src/modules/tags/graphql/schemas.ts (1)
13-16: Counts and createdAt additions look good.Field set aligns with resolvers and caching needs.
| tag: t.procedure.input(z.any()).mutation(async ({ ctx, input }) => { | ||
| const { action, _ids, tagIds, targetIds } = input; | ||
| const { models } = ctx; | ||
|
|
||
| let response = {}; | ||
|
|
||
| console.log('input', input); | ||
|
|
||
| if (action === 'count') { | ||
| response = await models.Customers.countDocuments({ | ||
| tagIds: { $in: _ids }, | ||
| }); | ||
| } | ||
|
|
||
| if (action === 'tagObject') { | ||
| await models.Customers.updateMany( | ||
| { _id: { $in: targetIds } }, | ||
| { $set: { tagIds } }, | ||
| ); | ||
|
|
||
| response = await models.Customers.find({ | ||
| _id: { $in: targetIds }, | ||
| }).lean(); | ||
| } | ||
|
|
||
| return response; | ||
| }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Validate input with zod; avoid console logs; append tags instead of replacing.
- Replace z.any() with a precise schema.
- Remove console.log in server code.
- Use $addToSet/$each to avoid clobbering existing tags.
- tag: t.procedure.input(z.any()).mutation(async ({ ctx, input }) => {
- const { action, _ids, tagIds, targetIds } = input;
+ tag: t.procedure
+ .input(
+ z.union([
+ z.object({ action: z.literal('count'), _ids: z.array(z.string().min(1)) }),
+ z.object({
+ action: z.literal('tagObject'),
+ targetIds: z.array(z.string().min(1)),
+ tagIds: z.array(z.string().min(1)),
+ }),
+ ])
+ )
+ .mutation(async ({ ctx, input }) => {
+ const { action } = input as any;
const { models } = ctx;
let response = {};
- console.log('input', input);
-
if (action === 'count') {
- response = await models.Customers.countDocuments({
- tagIds: { $in: _ids },
- });
+ const { _ids } = input as { _ids: string[] };
+ response = await models.Customers.countDocuments({ tagIds: { $in: _ids } });
}
if (action === 'tagObject') {
- await models.Customers.updateMany(
- { _id: { $in: targetIds } },
- { $set: { tagIds } },
- );
+ const { targetIds, tagIds } = input as { targetIds: string[]; tagIds: string[] };
+ await models.Customers.updateMany(
+ { _id: { $in: targetIds } },
+ { $addToSet: { tagIds: { $each: tagIds } } },
+ );
response = await models.Customers.find({
- _id: { $in: targetIds },
+ _id: { $in: (input as any).targetIds },
}).lean();
}
return response;
}),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| tag: t.procedure.input(z.any()).mutation(async ({ ctx, input }) => { | |
| const { action, _ids, tagIds, targetIds } = input; | |
| const { models } = ctx; | |
| let response = {}; | |
| console.log('input', input); | |
| if (action === 'count') { | |
| response = await models.Customers.countDocuments({ | |
| tagIds: { $in: _ids }, | |
| }); | |
| } | |
| if (action === 'tagObject') { | |
| await models.Customers.updateMany( | |
| { _id: { $in: targetIds } }, | |
| { $set: { tagIds } }, | |
| ); | |
| response = await models.Customers.find({ | |
| _id: { $in: targetIds }, | |
| }).lean(); | |
| } | |
| return response; | |
| }), | |
| tag: t.procedure | |
| .input( | |
| z.union([ | |
| z.object({ action: z.literal('count'), _ids: z.array(z.string().min(1)) }), | |
| z.object({ | |
| action: z.literal('tagObject'), | |
| targetIds: z.array(z.string().min(1)), | |
| tagIds: z.array(z.string().min(1)), | |
| }), | |
| ]) | |
| ) | |
| .mutation(async ({ ctx, input }) => { | |
| const { action } = input as any; | |
| const { models } = ctx; | |
| let response = {}; | |
| if (action === 'count') { | |
| const { _ids } = input as { _ids: string[] }; | |
| response = await models.Customers.countDocuments({ tagIds: { $in: _ids } }); | |
| } | |
| if (action === 'tagObject') { | |
| const { targetIds, tagIds } = input as { targetIds: string[]; tagIds: string[] }; | |
| await models.Customers.updateMany( | |
| { _id: { $in: targetIds } }, | |
| { $addToSet: { tagIds: { $each: tagIds } } }, | |
| ); | |
| response = await models.Customers.find({ | |
| _id: { $in: (input as any).targetIds }, | |
| }).lean(); | |
| } | |
| return response; | |
| }), |
🤖 Prompt for AI Agents
In backend/core-api/src/modules/contacts/trpc/customer.ts around lines 227 to
253, the mutation uses z.any(), logs input to console, and overwrites tagIds on
update; replace z.any() with a concrete zod schema specifying action as a union
of allowed strings and properly typed _ids, tagIds and targetIds arrays, remove
the console.log call, and change the updateMany to use $addToSet with $each (or
conditional logic to use $set only when intentional) so tagIds are appended
without clobbering existing tags; also validate inputs before DB calls and
return typed response.
| return await model.updateMany( | ||
| { _id: { $in: targetIds } }, | ||
| { $set: { tagIds } }, | ||
| { $set: { tagIds: tags.map((tag) => tag._id) } }, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not overwrite existing tags; add to set instead.
$set replaces tagIds wholesale, risking data loss. Use $addToSet with $each to append idempotently.
- return await model.updateMany(
- { _id: { $in: targetIds } },
- { $set: { tagIds: tags.map((tag) => tag._id) } },
- );
+ return await model.updateMany(
+ { _id: { $in: targetIds } },
+ { $addToSet: { tagIds: { $each: tags.map((t) => t._id) } } },
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return await model.updateMany( | |
| { _id: { $in: targetIds } }, | |
| { $set: { tagIds } }, | |
| { $set: { tagIds: tags.map((tag) => tag._id) } }, | |
| ); | |
| return await model.updateMany( | |
| { _id: { $in: targetIds } }, | |
| { $addToSet: { tagIds: { $each: tags.map((t) => t._id) } } }, | |
| ); |
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/graphql/mutations.ts around lines 70 to 73,
the current updateMany uses $set which overwrites tagIds; replace it with
$addToSet and $each so the new tag IDs are appended idempotently (no duplicates)
instead of replacing the existing array, i.e., use $addToSet: { tagIds: { $each:
tags.map(t => t._id) } } in the update operation.
| let contentType = type; | ||
|
|
||
| if (contentType === 'all') { | ||
| const contentTypes: string[] = await getContentTypes(serviceName); | ||
| filter.type = { $in: contentTypes }; | ||
| } else { | ||
| filter.type = type; | ||
| const [_pluginName, _moduleName, instanceId] = contentType.split(':'); | ||
|
|
||
| if (!instanceId && params.instanceId) { | ||
| contentType = `${contentType}:${params.instanceId}`; | ||
| } | ||
|
|
||
| filter.type = contentType; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Type-safe instanceId handling.
params.instanceId is used but not declared in ITagFilterQueryParams. Extend the type here or add it to the shared interface.
- if (type) {
+ if (type) {
let contentType = type;
const [_pluginName, _moduleName, instanceId] = contentType.split(':');
- if (!instanceId && params.instanceId) {
+ if (!instanceId && (params as any).instanceId) {
- contentType = `${contentType}:${params.instanceId}`;
+ contentType = `${contentType}:${(params as any).instanceId}`;
}
filter.type = contentType;
}Optional (preferred): change the signature of generateFilter to accept params: ITagFilterQueryParams & { instanceId?: string }.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let contentType = type; | |
| if (contentType === 'all') { | |
| const contentTypes: string[] = await getContentTypes(serviceName); | |
| filter.type = { $in: contentTypes }; | |
| } else { | |
| filter.type = type; | |
| const [_pluginName, _moduleName, instanceId] = contentType.split(':'); | |
| if (!instanceId && params.instanceId) { | |
| contentType = `${contentType}:${params.instanceId}`; | |
| } | |
| filter.type = contentType; | |
| } | |
| if (type) { | |
| let contentType = type; | |
| const [_pluginName, _moduleName, instanceId] = contentType.split(':'); | |
| if (!instanceId && (params as any).instanceId) { | |
| contentType = `${contentType}:${(params as any).instanceId}`; | |
| } | |
| filter.type = contentType; | |
| } |
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/graphql/queries.ts around lines 12 to 21,
params.instanceId is referenced but not declared on ITagFilterQueryParams;
update the function signature to accept params: ITagFilterQueryParams & {
instanceId?: string } (preferred) or add instanceId?: string to the shared
ITagFilterQueryParams interface so TypeScript knows instanceId may exist, then
adjust any callers if needed to pass the optional instanceId and ensure the
local logic uses the typed optional field.
| export const countDocuments = async ( | ||
| subdomain: string, | ||
| type: string, | ||
| _ids: string[], | ||
| ) => { | ||
| const [pluginName, moduleName] = type.split(':'); | ||
|
|
||
| if (!isEnabled(pluginName)) { | ||
| return 0; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Await isEnabled and guard undefined pluginName.
isEnabled is async and should only run when pluginName exists.
- if (!isEnabled(pluginName)) {
- return 0;
- }
+ if (pluginName && !(await isEnabled(pluginName))) {
+ return 0;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const countDocuments = async ( | |
| subdomain: string, | |
| type: string, | |
| _ids: string[], | |
| ) => { | |
| const [pluginName, moduleName] = type.split(':'); | |
| if (!isEnabled(pluginName)) { | |
| return 0; | |
| } | |
| export const countDocuments = async ( | |
| subdomain: string, | |
| type: string, | |
| _ids: string[], | |
| ) => { | |
| const [pluginName, moduleName] = type.split(':'); | |
| if (pluginName && !(await isEnabled(pluginName))) { | |
| return 0; | |
| } | |
| // …rest of the function… | |
| } |
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/utils.ts around lines 73 to 82, the code
calls isEnabled synchronously and without guarding against a missing pluginName;
change the logic to first check that pluginName is truthy (return 0 if not),
then await isEnabled(pluginName) and return 0 if it resolves false. Ensure you
add the await keyword before isEnabled and the early guard for undefined/empty
pluginName so the function only invokes isEnabled when a pluginName exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important
Looks good to me! 👍
Reviewed 60efd6f in 41 seconds. Click for details.
- Reviewed
48lines of code in3files - Skipped
0files when reviewing. - Skipped posting
3draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. backend/core-api/src/modules/contacts/trpc/customer.ts:231
- Draft comment:
Removed debug logging ('console.log("input", input)') which cleans up the output. Use a proper logging framework if needed in production. - Reason this comment was not posted:
Confidence changes required:0%<= threshold50%None
2. backend/core-api/src/modules/tags/graphql/mutations.ts:47
- Draft comment:
Updated tag query filter from 'isGroup: false' to 'isGroup: { $ne: true }' to cover cases where isGroup may be undefined. Confirm that this change meets the intended business logic for filtering non-group tags. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
3. backend/core-api/src/modules/tags/utils.ts:77
- Draft comment:
Removed the plugin enablement check (isEnabled) in countDocuments. Verify that countDocuments should always send the TRPC message regardless of plugin status. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
Workflow ID: wflow_U9koeaaEq3DcTD0K
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important
Looks good to me! 👍
Reviewed c28a60d in 1 minute and 14 seconds. Click for details.
- Reviewed
1219lines of code in17files - Skipped
0files when reviewing. - Skipped posting
3draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. frontend/core-ui/src/modules/settings/tags/graphql/mutations/tagsMutations.ts:49
- Draft comment:
Mismatch: The GIVE_TAGS mutation requires a $type variable, but the schema for tagsTag doesn’t define it. Ensure schema and mutation are in sync. - Reason this comment was not posted:
Comment was not on a location in the diff, so it can't be submitted as a review comment.
2. backend/core-api/src/modules/tags/db/models/Tags.ts:44
- Draft comment:
Minor: Remove trailing whitespace in error message ('Nested group is not allowed 2 '). - Reason this comment was not posted:
Comment was not on a location in the diff, so it can't be submitted as a review comment.
3. backend/core-api/src/modules/tags/utils.ts:59
- Draft comment:
Consider avoiding async callbacks in forEach: the async arrow in forEach isn’t awaited. Use a for...of loop or remove async if not needed. - Reason this comment was not posted:
Comment was not on a location in the diff, so it can't be submitted as a review comment.
Workflow ID: wflow_IWGqMNTxmocaQWhw
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/core-api/src/modules/tags/utils.ts (1)
42-42: Bug: $in expects an array; passing a string breaks the query.This likely returns wrong matches. Wrap tag._id in an array.
- const tags = await models.Tags.find({ relatedIds: { $in: tag._id } }); + const tags = await models.Tags.find({ relatedIds: { $in: [tag._id] } });
♻️ Duplicate comments (10)
backend/core-api/src/modules/tags/utils.ts (1)
17-28: Non-atomic union can clobber concurrent updates; use $addToSet/$each.Replace read-merge-$set with a single atomic add-to-set.
- await models.Tags.updateOne( - { _id: parentTag._id }, - { - $set: { - relatedIds: [ - ...new Set([...relatedIds, ...(parentTag.relatedIds || [])]), - ], - }, - }, - ); + await models.Tags.updateOne( + { _id: parentTag._id }, + { $addToSet: { relatedIds: { $each: relatedIds } } }, + );backend/core-api/src/modules/tags/db/models/Tags.ts (2)
15-61: Validation misinterprets undefined isGroup and can block benign updates; compute effective next state and prevent cycles.Current checks use doc.isGroup directly and will throw when updating a group without providing isGroup. Also improve messages and add cycle guard.
class Tag { - public static async validate(_id: string | null, doc: ITag) { - const { name, parentId, isGroup } = doc; - - const existingTag = await models.Tags.findOne({ - name, - _id: { $ne: _id } - }).lean(); - - if (existingTag) { - throw new Error(`A tag named ${name} already exists`); - } - - const tag = _id ? await models.Tags.findOne({ _id }).lean() : null; - - if (String(_id) === String(parentId)) { - throw new Error('Group cannot be itself'); - } - - if (parentId) { - const parentTag = await models.Tags.findOne({ _id: parentId }).lean(); - - if (!parentTag) { - throw new Error('Group not found'); - } - - if (!parentTag.isGroup) { - throw new Error('Parent tag must be a group'); - } - - if ((isGroup || tag?.isGroup) && parentTag?.isGroup) { - throw new Error('Nested group is not allowed 2 '); - } - } - - if (tag) { - const parentTag = await models.Tags.findOne({ _id: tag.parentId }).lean(); - const childTags = await models.Tags.find({ parentId: tag._id }).lean(); - - if (parentTag?.isGroup && isGroup) { - throw new Error('Nested group is not allowed 3'); - } - - if (!isGroup && childTags.length) { - throw new Error('Group has tags'); - } - } - } + public static async validate(_id: string | null, doc: ITag) { + const { name } = doc; + // 1) Unique name (exclude self) + const dup = await models.Tags.findOne({ name, ...(!!_id ? { _id: { $ne: _id } } : {}) }).lean(); + if (dup) throw new Error(`A tag named ${name} already exists`); + + // 2) Load existing and compute effective next state + const existing = _id ? await models.Tags.findOne({ _id }).lean() : null; + const hasIsGroup = Object.prototype.hasOwnProperty.call(doc, 'isGroup'); + const hasParentId = Object.prototype.hasOwnProperty.call(doc, 'parentId'); + const nextIsGroup = hasIsGroup ? !!doc.isGroup : !!existing?.isGroup; + const nextParentId = hasParentId ? doc.parentId : existing?.parentId; + + // 3) Self-parent + if (_id && nextParentId && String(_id) === String(nextParentId)) { + throw new Error('A tag cannot be its own parent'); + } + + // 4) Parent existence/type and cycle prevention + if (nextParentId) { + const parentTag = await models.Tags.findOne({ _id: nextParentId }).lean(); + if (!parentTag) throw new Error('Parent tag not found'); + if (!parentTag.isGroup) throw new Error('Parent tag must be a group'); + // Prevent cycles: parent cannot be a descendant of current + if (_id && (parentTag.relatedIds || []).includes(_id)) { + throw new Error('Cannot set a tag’s parent to its descendant'); + } + } + + // 5) Group cannot have a parent + if (nextIsGroup && nextParentId) { + throw new Error('Group tag cannot have a parent'); + } + + if (existing) { + // Disallow converting a nested tag into a group + if (!existing.isGroup && nextIsGroup && !!existing.parentId) { + throw new Error('Cannot convert a nested tag into a group'); + } + // Prevent demoting a group that already has children + if (existing.isGroup && !nextIsGroup) { + const childCount = await models.Tags.countDocuments({ parentId: _id }); + if (childCount > 0) { + throw new Error('Cannot convert a group with children into a non-group'); + } + } + } + }
83-101: Only recompute relatedIds when parent actually changes; avoid unnecessary remove/add.Unconditionally removing when a tag has children adds churn and risks transient inconsistency.
public static async updateTag(_id: string, doc: ITag) { await this.validate(_id, doc); const tag = await models.Tags.getTag(_id); - const childTags = await models.Tags.find({ parentId: tag._id }); - - if (childTags.length) { - await removeRelatedTagIds(models, tag); - } + const parentChanged = + Object.prototype.hasOwnProperty.call(doc, 'parentId') && + String(tag.parentId || '') !== String(doc.parentId || ''); + if (parentChanged) { + // Remove subtree from old ancestry + await removeRelatedTagIds(models, tag); + } const updated = await models.Tags.findOneAndUpdate( { _id: tag._id }, doc, { new: true, }, ); if (updated) { - await setRelatedTagIds(models, updated); + // Propagate to new ancestry (or refresh if other relations changed) + await setRelatedTagIds(models, updated); } return updated; }backend/core-api/src/modules/tags/graphql/mutations.ts (2)
44-51: LGTM: include legacy docs lacking isGroup.isGroup: { $ne: true } correctly includes pre‑migration tags.
70-73: Do not overwrite existing tagIds; append idempotently.$set replaces arrays and can drop existing tags under concurrent updates.
- return await model.updateMany( - { _id: { $in: targetIds } }, - { $set: { tagIds: tags.map((tag) => tag._id) } }, - ); + return await model.updateMany( + { _id: { $in: targetIds } }, + { $addToSet: { tagIds: { $each: tags.map((t) => t._id) } } }, + );backend/core-api/src/modules/tags/graphql/queries.ts (2)
9-10: Use document type in FilterQuery.Typing against params is incorrect; use the Mongo document type.
-import { FilterQuery } from 'mongoose'; +import { FilterQuery } from 'mongoose'; +import { ITagDocument } from 'erxes-api-shared/core-types'; @@ - const filter: FilterQuery<ITagFilterQueryParams> = { ...commonQuerySelector, type: { $in: [null, ''] } }; + const filter: FilterQuery<ITagDocument> = { ...commonQuerySelector, type: { $in: [null, ''] } };
31-33: Allow explicit isGroup=false.Current truthy check skips false.
- if (isGroup) { + if (typeof isGroup === 'boolean') { filter.isGroup = isGroup; }backend/core-api/src/modules/tags/graphql/schemas.ts (3)
27-35: Align queries with new model: droptype, removetagsGetTypes, and synctagsQueryCountfilters.Keep a single param block (searchValue, parentId, ids, excludeIds, isGroup, instanceId) for both
tagsandtagsQueryCount.-const queryParams = ` - type: String, +const queryParams = ` searchValue: String, parentId: String, ids: [String], excludeIds: Boolean, isGroup: Boolean, instanceId: String, sortField: String, ${GQL_CURSOR_PARAM_DEFS} `; export const queries = ` - tagsGetTypes: [JSON] tags(${queryParams}): TagsListResponse tagDetail(_id: String!): Tag - tagsQueryCount(type: String, searchValue: String): Int + tagsQueryCount(${queryParams}): Int `;Also applies to: 41-46
49-54: Remove legacytypefrom mutation inputs.Mutation inputs should mirror the new schema; keeping
typeinvites confusion.const mutationParams = ` - type: String, colorCode: String, parentId: String, isGroup: Boolean, description: String, `;
12-12: Remove or deprecate legacytypefield on Tag.PR objective says “remove legacy type”; keeping it creates API drift.
- type: String + # type is legacy; remove or explicitly deprecate + # type: String @deprecated(reason: "Legacy field; scheduled for removal")
🧹 Nitpick comments (18)
frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx (1)
26-37: Align selected fields with frontend types: add hasChildren or relax the type.ITag.hasChildren is required in Tag.ts, but this query doesn’t fetch it. Either select hasChildren here or make it optional in the type.
Apply one:
list { _id colorCode name parentId totalObjectCount objectCount type description isGroup createdAt + hasChildren }frontend/libs/ui-modules/src/modules/tags/types/Tag.ts (2)
16-16: Make hasChildren optional or ensure all queries include it.Current GraphQL queries don’t return hasChildren; making it required will cause type/usage drift.
- hasChildren: boolean; + hasChildren?: boolean;
27-43: Avoid relying on global React namespace for types.Import ReactNode explicitly; cleaner and works regardless of tsconfig “types”.
-import { MutationHookOptions, OperationVariables } from '@apollo/client'; +import { MutationHookOptions, OperationVariables } from '@apollo/client'; +import type { ReactNode } from 'react'; @@ export type ISelectTagsProviderProps = { tagType: string; targetIds?: string[]; value?: string[] | string; onValueChange?: (tags: string[] | string) => void; mode?: 'single' | 'multiple'; - children: React.ReactNode; + children: ReactNode;frontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsx (3)
13-15: Guard context usage; throw when used outside provider.Initialize context as undefined and enforce usage via a safe hook.
-export const TagContext = createContext<ITagContext>({} as ITagContext); +export const TagContext = createContext<ITagContext | undefined>(undefined); @@ -export const useTagContext = () => { - return useContext(TagContext); -}; +export const useTagContext = () => { + const ctx = useContext(TagContext); + if (!ctx) throw new Error('useTagContext must be used within TagProvider'); + return ctx; +};
15-23: Add explicit props interface and keep exports typed.Minor clarity/type hygiene.
-export const TagProvider = ({children}: {children: React.ReactNode}) => { +interface TagProviderProps { children: React.ReactNode } +export const TagProvider = ({ children }: TagProviderProps) => {
30-41: Stabilize context value with useMemo/useCallback to reduce renders.Prevents unnecessary re-renders of consumers.
+import { createContext, useContext, useState, useMemo, useCallback } from 'react'; @@ - const startAddingTag = () => setMode('adding-tag'); - const startAddingGroup = () => setMode('adding-group'); - const startAddingTagToGroup = (groupId: string) => { + const startAddingTag = useCallback(() => setMode('adding-tag'), []); + const startAddingGroup = useCallback(() => setMode('adding-group'), []); + const startAddingTagToGroup = useCallback((groupId: string) => { setTargetGroupId(groupId); setMode('adding-tag-to-group'); - }; - const cancel = () => { + }, []); + const cancel = useCallback(() => { setMode('idle'); setTargetGroupId(null); - }; + }, []); @@ - return ( - <TagContext.Provider value={{ - mode, - targetGroupId, - setMode, - startAddingTag, - startAddingGroup, - startAddingTagToGroup, - cancel - }}> + const value = useMemo(() => ({ + mode, + targetGroupId, + setMode, + startAddingTag, + startAddingGroup, + startAddingTagToGroup, + cancel, + }), [mode, targetGroupId, startAddingTag, startAddingGroup, startAddingTagToGroup, cancel]); + + return ( + <TagContext.Provider value={value}> {children} </TagContext.Provider> )backend/core-api/src/modules/tags/utils.ts (1)
59-69: Remove unnecessary async in forEach; build ops synchronously.Avoid misleading async usage.
- tags.forEach(async (t) => { + tags.forEach((t) => { const ids = (t.relatedIds || []).filter((id) => !relatedIds.includes(id)); doc.push({ updateOne: { filter: { _id: t._id }, update: { $set: { relatedIds: ids } }, }, }); });frontend/core-ui/src/modules/settings/tags/hooks/useTagsAdd.tsx (1)
14-19: Preserve caller callbacks and return the mutation promise.Currently overrides onError and doesn't return the promise, making callers unable to await and losing custom handlers.
Apply:
- addTag({ + return addTag({ ...options, variables, refetchQueries: ['Tags'], - onError(error) { - toast({ - title: error?.message || 'Failed to add tag', - variant: 'destructive', - }); - }, + onError(err) { + toast({ title: err?.message || 'Failed to add tag', variant: 'destructive' }); + options?.onError?.(err); + }, + onCompleted(data) { + options?.onCompleted?.(data); + } });frontend/core-ui/src/modules/settings/tags/components/TagsGroupsAddButtons.tsx (1)
4-14: Prefer function declaration per guidelines.Follow repo guidance “Use the function keyword for pure functions.”
-export const TagsGroupsAddButtons = () => { +export function TagsGroupsAddButtons() { const { startAddingTag, startAddingGroup } = useTagContext(); return ( <div className="flex gap-2"> <Button onClick={startAddingGroup} variant="outline"> Add Group </Button> <Button onClick={startAddingTag}>Add Tag</Button> </div> ); -}; +}backend/core-api/src/modules/tags/graphql/mutations.ts (1)
36-43: Type format check is good; consider clearer error.Message is fine; optionally include expected segments count for quicker debugging.
backend/core-api/src/modules/tags/graphql/queries.ts (2)
35-54: Variable shadowing of ids; rename for clarity.Shadowing params.ids with local ids hampers readability.
- let ids = [parentId, ...parentTag]; + let accIds = [parentId, ...parentTag]; @@ - ids = [...ids, ...childTag]; + accIds = [...accIds, ...childTag]; @@ - filter._id = { $in: ids }; + filter._id = { $in: accIds };
6-21: Type params to include optional instanceId.instanceId is accessed but not in ITagFilterQueryParams.
-const generateFilter = async ({ params, commonQuerySelector, models }) => { +const generateFilter = async ({ + params, + commonQuerySelector, + models, +}: { + params: ITagFilterQueryParams & { instanceId?: string }; + commonQuerySelector: Record<string, unknown>; + models: IContext['models']; +}) => {frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx (3)
201-212: Avoid console in UI flows; surface via toast.Replace console.error with toast for consistent UX.
- } catch (e) { - console.error(e.message); - } + } catch (e: any) { + toast({ title: e?.message || 'Failed to remove tag', variant: 'destructive' }); + }
120-141: Minor: drop redundant local logging; rely on onError toast.The extra console log is unnecessary.
- onError: (error) => { - console.log('error', error); - toast({ - title: error.message, - variant: 'destructive', - }); - }, + onError: (error) => + toast({ title: error.message, variant: 'destructive' }),
442-462: Date rendering: consider locale-aware formatting.Hardcoding 'en-US' may be inconsistent; prefer Intl.DateTimeFormat with user locale.
- return date.toLocaleDateString('en-US', { + return new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', year: 'numeric', - }); + }).format(date);frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx (2)
9-9: Use absolute import for internal module per guidelines.Replace relative import with absolute alias to match repo conventions.
-import { tagsColumns } from './TagsColumns'; +import { tagsColumns } from '@/settings/tags/components/TagsColumns';
26-35: Temporary row ID “new-item-temp” is brittle; namespace to avoid accidental collisions.Prefix with
temp:(or similar) so it can never clash with real IDs.- _id: 'new-item-temp', + _id: 'temp:new-item', @@ - _id: 'new-item-temp', + _id: 'temp:new-item',Also applies to: 93-104
backend/core-api/src/modules/tags/graphql/schemas.ts (1)
3-25: Strongly typetagsTagreturn payload.Avoid
JSONfor predictable client contracts.export const types = ` type Tag @key(fields: "_id") @cacheControl(maxAge: 3) { @@ createdAt: Date } + type TagTagResult { + modifiedCount: Int! + skippedCount: Int! + } + type TagsListResponse { list: [Tag] pageInfo: PageInfo totalCount: Int } `; @@ export const mutations = ` tagsAdd(name: String!, ${mutationParams}): Tag tagsEdit(_id: String!, name: String, ${mutationParams}): Tag - tagsTag(targetIds: [String!]!, tagIds: [String!]!): JSON + tagsTag(targetIds: [String!]!, tagIds: [String!]!): TagTagResult tagsRemove(_id: String!): JSON `;Also applies to: 56-61
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
backend/core-api/src/modules/contacts/trpc/customer.ts(1 hunks)backend/core-api/src/modules/tags/db/definitions/tags.ts(1 hunks)backend/core-api/src/modules/tags/db/models/Tags.ts(3 hunks)backend/core-api/src/modules/tags/graphql/customResolvers/tag.ts(1 hunks)backend/core-api/src/modules/tags/graphql/mutations.ts(3 hunks)backend/core-api/src/modules/tags/graphql/queries.ts(3 hunks)backend/core-api/src/modules/tags/graphql/schemas.ts(3 hunks)backend/core-api/src/modules/tags/utils.ts(2 hunks)frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx(1 hunks)frontend/core-ui/src/modules/settings/tags/components/TagsGroupsAddButtons.tsx(1 hunks)frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx(1 hunks)frontend/core-ui/src/modules/settings/tags/components/TagsSettingBreadcrumb.tsx(0 hunks)frontend/core-ui/src/modules/settings/tags/components/TagsSettingsFilter.tsx(0 hunks)frontend/core-ui/src/modules/settings/tags/graphql/mutations/tagsMutations.ts(2 hunks)frontend/core-ui/src/modules/settings/tags/hooks/useTagsAdd.tsx(2 hunks)frontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsx(1 hunks)frontend/core-ui/src/pages/settings/workspace/tags/TagsSettingPage.tsx(1 hunks)frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx(1 hunks)frontend/libs/ui-modules/src/modules/tags/types/Tag.ts(1 hunks)
💤 Files with no reviewable changes (2)
- frontend/core-ui/src/modules/settings/tags/components/TagsSettingBreadcrumb.tsx
- frontend/core-ui/src/modules/settings/tags/components/TagsSettingsFilter.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- backend/core-api/src/modules/tags/graphql/customResolvers/tag.ts
- backend/core-api/src/modules/contacts/trpc/customer.ts
- backend/core-api/src/modules/tags/db/definitions/tags.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx}: Use functional and declarative programming patterns; avoid classes.
Use TypeScript for all code; prefer interfaces over types.
Avoid enums; use maps instead.
Files:
frontend/core-ui/src/modules/settings/tags/hooks/useTagsAdd.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsGroupsAddButtons.tsxfrontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsxbackend/core-api/src/modules/tags/graphql/mutations.tsfrontend/libs/ui-modules/src/modules/tags/types/Tag.tsfrontend/core-ui/src/pages/settings/workspace/tags/TagsSettingPage.tsxfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsxfrontend/core-ui/src/modules/settings/tags/graphql/mutations/tagsMutations.tsfrontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsxbackend/core-api/src/modules/tags/utils.tsbackend/core-api/src/modules/tags/graphql/schemas.tsbackend/core-api/src/modules/tags/db/models/Tags.tsbackend/core-api/src/modules/tags/graphql/queries.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Avoid console logs.
Always use absolute paths when importing.
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
Use absolute path when import.
Files:
frontend/core-ui/src/modules/settings/tags/hooks/useTagsAdd.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsGroupsAddButtons.tsxfrontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsxbackend/core-api/src/modules/tags/graphql/mutations.tsfrontend/libs/ui-modules/src/modules/tags/types/Tag.tsfrontend/core-ui/src/pages/settings/workspace/tags/TagsSettingPage.tsxfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsxfrontend/core-ui/src/modules/settings/tags/graphql/mutations/tagsMutations.tsfrontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsxbackend/core-api/src/modules/tags/utils.tsbackend/core-api/src/modules/tags/graphql/schemas.tsbackend/core-api/src/modules/tags/db/models/Tags.tsbackend/core-api/src/modules/tags/graphql/queries.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{tsx,jsx}: Favor named exports for components.
Use declarative JSX.
Use Shadcn UI, Radix, and Tailwind for components and styling.
Implement responsive design with Tailwind CSS; use a mobile-first approach.
**/*.{tsx,jsx}: Structure files: exported component, subcomponents, helpers, static content, types.
Favor named exports for components.
Use declarative JSX.
Use Shadcn UI, Radix, and Tailwind for components and styling.
Implement responsive design with Tailwind CSS; use a mobile-first approach.
Files:
frontend/core-ui/src/modules/settings/tags/hooks/useTagsAdd.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsGroupsAddButtons.tsxfrontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsxfrontend/core-ui/src/pages/settings/workspace/tags/TagsSettingPage.tsxfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx
**/*.tsx
📄 CodeRabbit inference engine (.cursorrules)
**/*.tsx: Use functional components with TypeScript interfaces.
Minimize 'useEffect', and 'setState'.
Wrap client components in Suspense with fallback.
Use dynamic loading for non-critical components.
Limit 'use client': Favor server components and Next.js SSR. Use only for Web API access in small components. Avoid for data fetching or state management.
Files:
frontend/core-ui/src/modules/settings/tags/hooks/useTagsAdd.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsGroupsAddButtons.tsxfrontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsxfrontend/core-ui/src/pages/settings/workspace/tags/TagsSettingPage.tsxfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx
**/*.{webp,tsx,jsx}
📄 CodeRabbit inference engine (.cursorrules)
Optimize images: use WebP format, include size data, implement lazy loading.
Files:
frontend/core-ui/src/modules/settings/tags/hooks/useTagsAdd.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsGroupsAddButtons.tsxfrontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsxfrontend/core-ui/src/pages/settings/workspace/tags/TagsSettingPage.tsxfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx
🧬 Code graph analysis (9)
frontend/core-ui/src/modules/settings/tags/components/TagsGroupsAddButtons.tsx (2)
frontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsx (1)
useTagContext(45-47)frontend/libs/erxes-ui/src/components/button.tsx (1)
Button(43-57)
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx (16)
frontend/libs/ui-modules/src/modules/tags/hooks/useTags.tsx (1)
useTags(9-57)frontend/libs/erxes-ui/src/components/popover.tsx (1)
Popover(52-57)frontend/libs/erxes-ui/src/components/command.tsx (1)
Command(223-233)frontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsx (1)
useTagContext(45-47)frontend/core-ui/src/modules/settings/tags/hooks/useTagsAdd.tsx (1)
useTagsAdd(5-27)frontend/libs/erxes-ui/src/components/input.tsx (1)
Input(93-95)frontend/libs/erxes-ui/src/hooks/use-confirm.tsx (1)
useConfirm(133-181)frontend/libs/erxes-ui/src/hooks/use-query-state.tsx (1)
useQueryState(6-49)frontend/core-ui/src/modules/settings/tags/hooks/useRemoveTag.tsx (1)
useRemoveTag(5-30)frontend/core-ui/src/modules/settings/tags/hooks/useTagsEdit.tsx (1)
useTagsEdit(5-41)frontend/libs/erxes-ui/src/modules/record-table/index.ts (1)
RecordTable(23-40)frontend/libs/erxes-ui/src/components/combobox.tsx (1)
Combobox(200-208)frontend/libs/erxes-ui/src/modules/record-table/components/RecordTableCellInline.tsx (1)
RecordTableInlineCell(100-104)frontend/libs/erxes-ui/src/modules/record-table/components/RecordTableTree.tsx (1)
RecordTableTree(131-136)frontend/libs/erxes-ui/src/components/text-overflow.tsx (1)
TextOverflowTooltip(10-56)frontend/libs/erxes-ui/src/components/textarea.tsx (1)
Textarea(5-19)
backend/core-api/src/modules/tags/graphql/mutations.ts (2)
backend/core-api/src/modules/tags/graphql/queries.ts (1)
tags(86-104)backend/core-api/src/modules/automations/graphql/resolvers/customResolver/automations.ts (1)
tags(21-23)
frontend/core-ui/src/pages/settings/workspace/tags/TagsSettingPage.tsx (4)
frontend/libs/erxes-ui/src/hooks/use-query-state.tsx (1)
useQueryState(6-49)frontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsx (1)
TagProvider(15-43)frontend/core-ui/src/modules/settings/tags/components/TagsGroupsAddButtons.tsx (1)
TagsGroupsAddButtons(4-14)frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx (1)
TagsRecordTable(11-146)
frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx (5)
frontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsx (1)
useTagContext(45-47)frontend/libs/erxes-ui/src/hooks/use-query-state.tsx (1)
useMultiQueryState(57-98)frontend/libs/ui-modules/src/modules/tags/hooks/useTags.tsx (1)
useTags(9-57)frontend/libs/ui-modules/src/modules/tags/types/Tag.ts (1)
ITag(3-18)frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx (1)
tagsColumns(293-472)
backend/core-api/src/modules/tags/utils.ts (2)
backend/erxes-api-shared/src/core-types/modules/tags/tag.ts (1)
ITagDocument(13-16)backend/erxes-api-shared/src/utils/trpc/index.ts (1)
sendTRPCMessage(42-68)
backend/core-api/src/modules/tags/graphql/schemas.ts (2)
backend/core-api/src/modules/contacts/graphql/schemas/company.ts (1)
mutations(123-128)backend/core-api/src/modules/products/graphql/schemas/product.ts (2)
mutations(121-127)mutationParams(99-119)
backend/core-api/src/modules/tags/db/models/Tags.ts (2)
backend/erxes-api-shared/src/core-types/modules/tags/tag.ts (1)
ITag(3-11)backend/core-api/src/modules/tags/utils.ts (2)
setRelatedTagIds(6-35)removeRelatedTagIds(38-71)
backend/core-api/src/modules/tags/graphql/queries.ts (2)
backend/core-api/src/modules/tags/@types/tag.ts (1)
ITagFilterQueryParams(6-15)backend/erxes-api-shared/src/utils/service-discovery.ts (2)
getPlugins(20-25)getPlugin(30-49)
🪛 Biome (2.1.2)
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx
[error] 311-311: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 313-313: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 314-314: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 315-315: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 317-317: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 321-321: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
🪛 ast-grep (0.38.6)
backend/core-api/src/modules/tags/graphql/queries.ts
[warning] 23-23: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(.*${searchValue}.*, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: tourism_api-ci
- GitHub Check: operation_ui-ci
- GitHub Check: frontline_ui-ci
- GitHub Check: core-ui
- GitHub Check: sales_ui-ci
- GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (13)
frontend/libs/ui-modules/src/modules/tags/types/Tag.ts (1)
13-14: Remove legacy “order” if backend dropped it.PR says order was removed; keep types in sync to prevent confusion.
- order?: string;backend/core-api/src/modules/tags/db/models/Tags.ts (1)
112-124: LGTM: child unparenting on delete matches new behavior.Unsetting parentId for children aligns with the PR’s deletion policy.
frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx (1)
11-14: Confirm backend GraphQL types for $ids / $excludeIds / $isGroupGrep didn't reveal a tags query schema; resolvers treat ids as string[] (e.g. backend/plugins/content_api/src/modules/knowledgebase/graphql/resolvers/queries.ts and backend/plugins/content_api/src/modules/portal/graphql/resolvers/queries/tag.ts). Verify the server schema declares ids as [String] (or [ID!]) and excludeIds as Boolean to match frontend ($ids: [String], $excludeIds: Boolean, $isGroup: Boolean).
backend/core-api/src/modules/tags/utils.ts (1)
73-99: Add defaultValue: 0 and forward subdomain header — verification requiredsendTRPCMessage returns its defaultValue when a plugin is disabled; this call omits defaultValue (risking undefined). The subdomain parameter is unused — confirm whether it must be forwarded for multi-tenant routing (e.g., options: { headers: { 'x-subdomain': subdomain } }).
return await sendTRPCMessage({ pluginName, method: 'mutation', module: MODULE_NAMES[moduleName] || moduleName, action: 'tag', input: { type, _ids, action: 'count', }, + defaultValue: 0, + // options: { headers: { 'x-subdomain': subdomain } }, // if required by downstream });Repo search did not locate sendTRPCMessage — confirm its signature/behavior and whether subdomain forwarding is required.
frontend/core-ui/src/modules/settings/tags/hooks/useTagsAdd.tsx (1)
2-2: LGTM: user‑visible error instead of console.Good move to toast errors.
frontend/core-ui/src/pages/settings/workspace/tags/TagsSettingPage.tsx (1)
14-28: LGTM: context provider wrap and header actions are coherent.Layout and provider placement look consistent with the new Tag context.
backend/core-api/src/modules/tags/graphql/queries.ts (1)
9-21: Confirm default type filter behavior.Setting type: { $in: [null, ''] } by default excludes typed tags unless type is provided. Is this intentional for this endpoint?
frontend/core-ui/src/modules/settings/tags/graphql/mutations/tagsMutations.ts (3)
3-23: LGTM: tagsAdd inputs aligned (type optional, description, isGroup).Matches backend shape in this PR.
Please confirm schema is deployed before merging to avoid client/server mismatch.
25-47: LGTM: tagsEdit fields aligned.Consistent with tagsAdd.
49-57: LGTM: tagsTag unchanged.No issues spotted.
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx (1)
32-45: Lazy-load groups only when open: good.skip: !open is a nice touch to reduce queries.
frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx (2)
66-72: Sorting: normalize case/locale for consistent UX.Use
localeComparewith options to avoid case-order surprises.
[ suggest_nitpick_refactor ]- const sortByName = (a: ITag, b: ITag) => a.name.localeCompare(b.name); + const sortByName = (a: ITag, b: ITag) => + a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
48-63: Orphan child handling: confirm behavior when parent tag is missing.
Children with a non-existent parentId are silently dropped by this loop; either enforce the invariant that every parentId has a matching tag._id or add a fallback (e.g., standaloneTagsSet.add(tag._id) and console.warn / surface the orphan in the UI).
File: frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx (lines 48–63).
Previous verification script produced no output — verify dataset invariants or run a repo check that lists tags whose parentId has no matching _id.
| if (searchValue) { | ||
| filter.name = new RegExp(`.*${searchValue}.*`, 'i'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escape user input in regex to avoid ReDoS.
Variable-based RegExp is vulnerable. Use a bounded, escaped $regex.
- if (searchValue) {
- filter.name = new RegExp(`.*${searchValue}.*`, 'i');
- }
+ if (searchValue) {
+ const ESCAPE_RE = /[.*+?^${}()|[\]\\]/g;
+ const safe = String(searchValue).slice(0, 100).replace(ESCAPE_RE, '\\$&');
+ filter.name = { $regex: safe, $options: 'i' };
+ }And mirror the same change in tagsQueryCount.
Also applies to: 123-125
🧰 Tools
🪛 ast-grep (0.38.6)
[warning] 23-23: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(.*${searchValue}.*, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/graphql/queries.ts around lines 23-25 (and
similarly at lines 123-125 for tagsQueryCount), the code builds a RegExp
directly from user input which can cause ReDoS and unintended pattern behavior;
replace the dynamic RegExp with a MongoDB $regex filter using an escaped version
of searchValue and a bounded pattern (escape special regex characters in
searchValue, then use { $regex: escapedValue, $options: 'i' } or build a safe
anchored pattern) to prevent regex injection and improve performance, and apply
the same change in the tagsQueryCount location.
| export const mutations = ` | ||
| tagsAdd(${mutationParams}): Tag | ||
| tagsEdit(_id: String!, ${mutationParams}): Tag | ||
| tagsAdd(name: String!, ${mutationParams}): Tag | ||
| tagsEdit(_id: String!, name: String, ${mutationParams}): Tag | ||
| tagsTag(targetIds: [String!]!, tagIds: [String!]!): JSON | ||
| tagsRemove(_id: String!): JSON | ||
| tagsTag(type: String!, targetIds: [String!]!, tagIds: [String!]!): JSON | ||
| tagsMerge(sourceId: String!, destId: String!): Tag | ||
| `; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Schema/resolver parity check for tagsTag and removal of type.
Ensure resolvers no longer destructure or use type and that tagsTag returns the new payload.
🏁 Script executed:
#!/bin/bash
# Verify no resolver still expects or uses `type`
rg -nP '\btags(Tag|Add|Edit)\b' backend/core-api/src/modules/tags/graphql -C2
rg -nP '\btype\s*:' backend/core-api/src/modules/tags/graphql -C2
rg -nP '\btagsQueryCount\s*\(' backend/core-api/src/modules/tags/graphql -C3
# Show tagsTag resolver for payload shape
rg -nP 'tagsTag\s*\(' backend/core-api/src/modules/tags/graphql -C5Length of output: 7909
Fix schema/resolver parity for tagsTag: remove type usage or add type to the mutation
- schemas: backend/core-api/src/modules/tags/graphql/schemas.ts — tagsTag signature currently has no
type(tagsTag(targetIds: [String!]!, tagIds: [String!]!): JSON). - resolver: backend/core-api/src/modules/tags/graphql/mutations.ts — tagsTag still destructures
typefrom args and uses it when building the activity/payload. Removetypefrom the resolver args/usage or addtype: Stringback to the mutation signature to restore parity. - Also align the resolver return with the expected/new tagsTag payload (update schema return type or change resolver return accordingly).
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/graphql/schemas.ts around lines 56-61, the
tagsTag mutation signature lacks the `type` argument but the resolver expects
and uses `type`; either add `type: String` to the mutation signature
(tagsTag(targetIds: [String!]!, tagIds: [String!]!, type: String): JSON) or
remove `type` destructuring/usage from
backend/core-api/src/modules/tags/graphql/mutations.ts so resolver no longer
expects it; additionally ensure the resolver returns match the schema by
updating the schema return type to the actual payload shape the resolver
produces or adjust the resolver to return a plain JSON matching the declared
JSON return type.
| const { mode } = useTagContext(); | ||
| const row = cell.row.original; | ||
|
|
||
| if ( | ||
| (mode === 'adding-tag' || | ||
| mode === 'adding-group' || | ||
| mode === 'adding-tag-to-group') && | ||
| row._id === 'new-item-temp' | ||
| ) { | ||
| return <NewItemCell />; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix hook order: move hooks above early return.
Hooks are called after a conditional return; violates Rules of Hooks (linter errors).
- const { mode } = useTagContext();
- const row = cell.row.original;
-
- if (
- (mode === 'adding-tag' ||
- mode === 'adding-group' ||
- mode === 'adding-tag-to-group') &&
- row._id === 'new-item-temp'
- ) {
- return <NewItemCell />;
- }
-
- const { tagsEdit, loading } = useTagsEdit();
- const { _id, name, isGroup, order, hasChildren } = row;
- const [editingTagId, setEditingTagId] = useQueryState('editingTagId');
- const [open, setOpen] = React.useState<boolean>(false);
- const [_name, setName] = React.useState<string>(name);
+ const { mode } = useTagContext();
+ const row = cell.row.original;
+ const { tagsEdit, loading } = useTagsEdit();
+ const [editingTagId, setEditingTagId] = useQueryState('editingTagId');
+ const [open, setOpen] = React.useState<boolean>(false);
+ const [_name, setName] = React.useState<string>(row.name);
+
+ if (
+ (mode === 'adding-tag' ||
+ mode === 'adding-group' ||
+ mode === 'adding-tag-to-group') &&
+ row._id === 'new-item-temp'
+ ) {
+ return <NewItemCell />;
+ }Also applies to: 311-321
🤖 Prompt for AI Agents
In frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx around
lines 299-321, hooks (e.g., useTagContext and any other React hooks used later
in this block) are being called after an early conditional return which violates
the Rules of Hooks; move all hook calls to the top of this component/function
(before the if that returns <NewItemCell />), assign their results to local
variables, and then perform the existing conditional check/early return using
those variables so no hook is invoked conditionally.
| order: tag.name, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Order path built from names risks collisions and “/” bugs; switch to stable, ID‑based keys sorted by name.
Current order uses names (and /) which can collide, break when names contain /, and are not stable on rename. Use a sortable, unique key that embeds lowercase name for ordering and IDs for uniqueness.
- order: tag.name,
+ // Top-level standalone tag
+ order: `T:${tag.name.toLowerCase()}#${tag._id}`,
@@
- order: group.name,
+ // Group node
+ order: `G:${group.name.toLowerCase()}#${group._id}`,
@@
- order: `${group.name}/new-item-temp`,
+ // Placeholder under group
+ order: `G:${group.name.toLowerCase()}#${group._id}/T:__new__#new-item-temp`,
@@
- order: `${group.name}/${child.name}`,
+ // Child under group
+ order: `G:${group.name.toLowerCase()}#${group._id}/T:${child.name.toLowerCase()}#${child._id}`,Also applies to: 91-91, 102-103, 111-112
🤖 Prompt for AI Agents
In frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx
around lines 77-78 (also apply same change at 91, 102-103, 111-112): the current
order value is built from tag names which can collide, break on names containing
"/" and is unstable on rename; replace it with a stable, unique ID-based
ordering key that still sorts by name by composing a sortable lowercase name
prefix and the unique tag ID (e.g. lowercase name + separator + id) so ordering
uses the name but uniqueness uses the id; ensure any downstream sort/comparison
uses the name portion for human ordering and the full composed key for
uniqueness and avoid using raw names or "/" characters alone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Changes requested ❌
Reviewed 1c544b7 in 2 minutes and 23 seconds. Click for details.
- Reviewed
195lines of code in6files - Skipped
0files when reviewing. - Skipped posting
8draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. backend/core-api/src/commands/migrateTags.ts:29
- Draft comment:
Migration now sets 'order' to an empty string via $set instead of unsetting it. Confirm that preserving the field (with an empty value) is the intended behavior. - Reason this comment was not posted:
Comment looked like it was already resolved.
2. backend/core-api/src/modules/tags/db/definitions/tags.ts:16
- Draft comment:
The 'order' field is now explicitly defined in the schema. Ensure its documentation reflects that it is computed and not intended for manual input. - Reason this comment was not posted:
Comment did not seem useful. Confidence is useful =0%<= threshold50%The comment is asking the author to ensure that the documentation reflects a change in the schema. This is a request for confirmation and ensuring documentation, which violates the rules.
3. backend/core-api/src/modules/tags/db/models/Tags.ts:102
- Draft comment:
When 'tag.order' is empty, the regex will match all documents. Verify that this behavior during update operations is intended. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% The comment raises a theoretical edge case, but examining the code shows that: 1) order is always set when creating/updating tags via generateOrder() 2) the pattern always ends with "/" 3) the code uses escapeRegExp for safety. The concern appears unfounded since the code handles this case properly. I could be wrong about order never being empty - there might be legacy data or other code paths I'm not seeing that could create tags without orders. Even if there were legacy tags without orders, the || '' fallback makes this safe - it would match an empty string which is reasonable behavior for finding children of an unordered parent. The comment should be deleted as it raises a speculative concern about an edge case that is properly handled by the code.
4. backend/core-api/src/modules/tags/db/models/Tags.ts:168
- Draft comment:
The generateOrder method concatenates the parent's order with the tag name. Ensure it correctly handles cases when parentId is undefined and that the tag name is sanitized if necessary. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 20% vs. threshold = 50% Looking at the code: 1) The validate() method already checks for duplicate tag names 2) The order string is only used internally for maintaining hierarchy 3) The name is already sanitized by validation before reaching this point 4) The only dangerous character '/' is intentionally used as a delimiter 5) encodeURIComponent would actually make the order strings less readable and harder to debug The comment raises a valid security concern about string sanitization. Perhaps there are edge cases with special characters in tag names that could cause issues. The validation and existing checks are sufficient - encoding would actually make the order strings less useful while not providing meaningful security benefits since this is an internal implementation detail. The comment should be removed as the suggested change is unnecessary - the existing implementation is correct and encoding would reduce usability without clear benefits.
5. backend/core-api/src/modules/tags/graphql/queries.ts:102
- Draft comment:
The pagination now includes 'orderBy: { order: 1 }' to sort tags. Confirm that there is an index supporting this sort for optimal performance. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 20% vs. threshold = 50% While performance optimization suggestions can be valuable, this comment is speculative without evidence of a performance problem. It's asking for verification rather than pointing out a clear issue. We don't have visibility into the database schema or indexes, so we can't be certain if this is actually a problem. The comment raises a valid performance consideration. Missing indexes on sorted fields can significantly impact query performance. However, the comment violates our rules by asking for verification ("Confirm that...") rather than making a clear, actionable statement. It's also speculative without evidence of an actual performance issue. The comment should be deleted because it's phrased as a verification request and lacks concrete evidence of a problem.
6. backend/core-api/src/modules/tags/graphql/schemas.ts:14
- Draft comment:
The GraphQL schema now exposes the 'order' field. Ensure that consumers of this API correctly interpret its computed value. - Reason this comment was not posted:
Comment did not seem useful. Confidence is useful =0%<= threshold50%The comment is asking the PR author to ensure that consumers of the API interpret the 'order' field correctly. This falls under the category of asking the author to ensure behavior, which is against the rules. The comment does not provide a specific suggestion or point out a specific issue with the code.
7. backend/erxes-api-shared/src/core-types/modules/tags/tag.ts:11
- Draft comment:
The core-types now include an optional 'order' field. Verify that documentation and downstream types are updated accordingly. - Reason this comment was not posted:
Comment did not seem useful. Confidence is useful =0%<= threshold50%This comment is asking the PR author to verify that documentation and downstream types are updated, which falls under the rule of not asking the author to ensure things are updated or verified. It doesn't provide a specific suggestion or point out a specific issue with the code.
8. backend/core-api/src/modules/tags/db/models/Tags.ts:58
- Draft comment:
There appears to be an extra '3' in the error message on line 58 ('Nested group is not allowed 3'). Please verify if this is intentional or a typographical error. - Reason this comment was not posted:
Comment was not on a location in the diff, so it can't be submitted as a review comment.
Workflow ID: wflow_DKw7llAfTHNT1968
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
| // updating child tag order | ||
| childTags.forEach((childTag) => { | ||
| // updating child categories order | ||
| childTags.forEach(async (childTag) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The forEach callback in updateTag is declared async although no await is used. Remove the async modifier to avoid confusion.
| childTags.forEach(async (childTag) => { | |
| childTags.forEach((childTag) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (9)
backend/core-api/src/modules/tags/graphql/queries.ts (4)
9-12: Use document type in FilterQuery generic.FilterQuery should target the Mongo document shape, not params. Switch to ITagDocument.
- const filter: FilterQuery<ITagFilterQueryParams> = { + const filter: FilterQuery<ITagDocument> = { ...commonQuerySelector, type: { $in: [null, ''] }, };Also import:
import { ITagDocument } from 'erxes-api-shared/core-types';
34-35: Allow explicit isGroup=false.Current guard drops false.
- if (isGroup) { - filter.isGroup = isGroup; - } + if (typeof isGroup === 'boolean') { + filter.isGroup = isGroup; + }
19-21: Type-safe instanceId handling.params.instanceId isn’t in ITagFilterQueryParams. Extend the type or narrow via a typed cast.
Option A (preferred): add instanceId?: string to ITagFilterQueryParams (shared types).
Option B (local narrow):
- if (!instanceId && params.instanceId) { - contentType = `${contentType}:${params.instanceId}`; + const instId = (params as { instanceId?: string }).instanceId; + if (!instanceId && instId) { + contentType = `${contentType}:${instId}`; }
26-28: Escape user input in regex (ReDoS risk).new RegExp(
.*${searchValue}.*, 'i') is user‑controlled and vulnerable. Use escaped, bounded $regex.- if (searchValue) { - filter.name = new RegExp(`.*${searchValue}.*`, 'i'); - } + if (searchValue) { + const safe = escapeRegExp(String(searchValue)).slice(0, 100); + filter.name = { $regex: safe, $options: 'i' }; + }- if (searchValue) { - selector.name = new RegExp(`.*${searchValue}.*`, 'i'); - } + if (searchValue) { + const safe = escapeRegExp(String(searchValue)).slice(0, 100); + selector.name = { $regex: safe, $options: 'i' }; + }Add to existing utils import:
-import { cursorPaginate, getPlugin, getPlugins } from 'erxes-api-shared/utils'; +import { cursorPaginate, getPlugin, getPlugins, escapeRegExp } from 'erxes-api-shared/utils';Also applies to: 129-131
backend/core-api/src/modules/tags/db/definitions/tags.ts (2)
24-24: Fix ineffective index and add targeted indexes.Leading with _id makes it unusable for common filters. Add indexes matching query patterns and scoped uniqueness.
-tagSchema.index({ _id: 1, name: 1, parentId: 1, type: 1 }); +// Hierarchy and filtering +tagSchema.index({ parentId: 1 }); +tagSchema.index({ type: 1, isGroup: 1 }); +tagSchema.index({ parentId: 1, type: 1, name: 1 }); +tagSchema.index({ relatedIds: 1 }); +// Scoped uniqueness (adjust to include tenant/org scope if present) +tagSchema.index({ name: 1, parentId: 1 }, { unique: true });If a unique index on name exists in prod, migrate: drop it before creating the compound unique.
8-8: Avoid global unique on name; scope uniqueness to parent (and tenant if applicable).Global unique blocks same-name tags under different groups/tenants.
- name: { type: String, label: 'Name', unique: true }, + name: { type: String, label: 'Name' },Follow-up index below.
backend/core-api/src/modules/tags/graphql/schemas.ts (1)
46-46: Align tagsQueryCount signature with tags filters.Expose the same filters (incl. isGroup/instanceId) and keep parity with resolver using generateFilter.
- tagsQueryCount(type: String, searchValue: String): Int + tagsQueryCount(${queryParams}): IntAlternatively, define a slimmer count param block mirroring tags’ filter fields.
backend/core-api/src/modules/tags/db/models/Tags.ts (2)
99-106: Fix relatedIds propagation on parent move (stale ancestry).removeRelatedTagIds runs only when childTags.length > 0. Also detect parent change and always remove from old ancestry before adding to new.
- const childTags = await models.Tags.find({ + const childTags = await models.Tags.find({ $and: [ { _id: { $ne: _id } }, { order: { $regex: new RegExp(escapeRegExp(tag.order || ''), 'i') } }, ], }); - if (childTags.length) { + if (childTags.length) { const bulkDoc: Array<{ updateOne: { filter: { _id: string }; update: { $set: { order: string } }; }; }> = []; - // updating child categories order - childTags.forEach(async (childTag) => { + // updating child categories order + childTags.forEach((childTag) => { let childOrder = childTag.order || ''; childOrder = childOrder.replace(tag.order || '', order); bulkDoc.push({ updateOne: { filter: { _id: childTag._id }, update: { $set: { order: childOrder } }, }, }); }); await models.Tags.bulkWrite(bulkDoc); - - await removeRelatedTagIds(models, tag); } + const parentChanged = + Object.prototype.hasOwnProperty.call(doc, 'parentId') && + tag.parentId !== doc.parentId; + if (parentChanged) { + await removeRelatedTagIds(models, tag); + } + const updated = await models.Tags.findOneAndUpdate( { _id: tag._id }, { ...doc, order, }, { new: true, }, ); if (updated) { await setRelatedTagIds(models, updated); }Also applies to: 128-131, 133-142, 144-146
16-65: Consolidate validation: uniqueness scope, self/parent checks, group-with-parent, cycles, and demotion with children.Current rules miss group-with-parent and cycle prevention, duplicate some checks, and enforce global name uniqueness.
- public static async validate(_id: string | null, doc: ITag) { - const { name, parentId, isGroup } = doc; - - const existingTag = await models.Tags.findOne({ - name, - _id: { $ne: _id }, - }).lean(); - - if (existingTag) { - throw new Error(`A tag named ${name} already exists`); - } - - const tag = _id ? await models.Tags.findOne({ _id }).lean() : null; - - if (String(_id) === String(parentId)) { - throw new Error('Group cannot be itself'); - } - - if (parentId) { - const parentTag = await models.Tags.findOne({ _id: parentId }).lean(); - - if (!parentTag) { - throw new Error('Group not found'); - } - - if (!parentTag.isGroup) { - throw new Error('Parent tag must be a group'); - } - - if ((isGroup || tag?.isGroup) && parentTag?.isGroup) { - throw new Error('Nested group is not allowed 2 '); - } - } - - if (tag) { - const parentTag = await models.Tags.findOne({ - _id: tag.parentId, - }).lean(); - - const childTags = await models.Tags.find({ parentId: tag._id }).lean(); - - if (parentTag?.isGroup && isGroup) { - throw new Error('Nested group is not allowed 3'); - } - - if (!isGroup && childTags.length) { - throw new Error('Group has tags'); - } - } - } + public static async validate(_id: string | null, doc: ITag) { + const { name, parentId, isGroup } = doc; + + const existing = _id ? await models.Tags.findOne({ _id }).lean() : null; + const hasIsGroup = Object.prototype.hasOwnProperty.call(doc, 'isGroup'); + const hasParentId = Object.prototype.hasOwnProperty.call(doc, 'parentId'); + const nextIsGroup = hasIsGroup ? !!isGroup : !!existing?.isGroup; + const nextParentId = hasParentId ? parentId : existing?.parentId; + + // Uniqueness scoped to parent (aligns with schema index) + const nameQuery: any = { name }; + if (nextParentId) { + nameQuery.parentId = nextParentId; + } else { + nameQuery.$or = [ + { parentId: { $exists: false } }, + { parentId: null }, + { parentId: '' }, + ]; + } + if (_id) nameQuery._id = { $ne: _id }; + const dup = await models.Tags.findOne(nameQuery).lean(); + if (dup) throw new Error('There is already a tag with this name'); + + // Self-parent + if (_id && nextParentId && String(_id) === String(nextParentId)) { + throw new Error('A tag cannot be its own parent'); + } + + // Parent existence, type, and cycle prevention + if (nextParentId) { + const parentTag = await models.Tags.findOne({ _id: nextParentId }).lean(); + if (!parentTag) throw new Error('Parent tag not found'); + if (!parentTag.isGroup) throw new Error('Parent tag must be a group'); + if (_id && (parentTag.relatedIds || []).includes(_id)) { + throw new Error('Cannot set a tag’s parent to its descendant'); + } + } + + // Group cannot have a parent + if (nextIsGroup && nextParentId) { + throw new Error('Group tag cannot have a parent'); + } + + if (existing) { + // Disallow converting a nested tag into a group + if (!existing.isGroup && nextIsGroup && !!existing.parentId) { + throw new Error('Cannot convert a nested tag into a group'); + } + // Prevent demoting a group that already has children + if (existing.isGroup && !nextIsGroup) { + const childCount = await models.Tags.countDocuments({ parentId: existing._id }); + if (childCount > 0) { + throw new Error('Cannot convert a group with children into a non-group'); + } + } + } + }
🧹 Nitpick comments (7)
backend/core-api/src/modules/tags/graphql/queries.ts (3)
30-31: Don’t overwrite _id filter when both ids and parentId are provided.parentId path replaces filter._id built from ids. Intersect or compose via $and for correctness.
Example change near Line 56:
- filter._id = { $in: ids }; + const parentScope = { _id: { $in: ids } }; + filter.$and = filter.$and ? [...filter.$and, parentScope] : [parentScope];Also applies to: 56-56
112-134: Reuse generateFilter for count; keep API parity with tags.Duplicated filter logic risks drift. Reuse generateFilter for tagsQueryCount and accept the same params (including instanceId). Update schema accordingly.
- async tagsQueryCount( - _parent: undefined, - { - type, - searchValue, - }: { - type: string; - searchValue?: string; - }, - { models, commonQuerySelector }: IContext, - ) { - const selector: any = { ...commonQuerySelector }; - - if (type) { - selector.type = type; - } - - if (searchValue) { - selector.name = new RegExp(`.*${searchValue}.*`, 'i'); - } - - return models.Tags.countDocuments(selector); - }, + async tagsQueryCount( + _parent: undefined, + params: ITagFilterQueryParams & { instanceId?: string }, + { models, commonQuerySelector }: IContext, + ) { + const selector = await generateFilter({ params, commonQuerySelector, models }); + return models.Tags.countDocuments(selector); + },
6-6: Add proper TypeScript types to generateFilter signature.Improves safety and editor help.
-const generateFilter = async ({ params, commonQuerySelector, models }) => { +const generateFilter = async ({ + params, + commonQuerySelector, + models, +}: { + params: ITagFilterQueryParams & { instanceId?: string }; + commonQuerySelector: FilterQuery<ITagDocument>; + models: IContext['models']; +}) => {backend/core-api/src/modules/tags/db/definitions/tags.ts (1)
16-16: Consider indexing order if frequently sorted.Optional, but aligns with default orderBy: { order: 1 }.
+tagSchema.index({ order: 1 });backend/core-api/src/modules/tags/db/models/Tags.ts (1)
115-115: Remove unnecessary async in forEach.No await inside; avoid misleading async.
- childTags.forEach(async (childTag) => { + childTags.forEach((childTag) => {backend/core-api/src/modules/tags/graphql/schemas.ts (2)
60-60: Prefer a typed payload for tagsTag instead of JSON.Define a minimal result type (e.g., modifiedCount, skippedCount) to stabilize the API.
Example:
export const types = ` + type TagTagResult { modifiedCount: Int!, skippedCount: Int! } `; export const mutations = ` - tagsTag(targetIds: [String!]!, tagIds: [String!]!): JSON + tagsTag(targetIds: [String!]!, tagIds: [String!]!): TagTagResult `;Update resolver return accordingly.
12-12: If planning to remove legacy “type”, deprecate the field now.Adds a migration path without breaking clients.
- type: String + type: String @deprecated(reason: "Legacy field; scheduled for removal")If keeping “type” long term, ignore this.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
backend/core-api/src/commands/migrateTags.ts(1 hunks)backend/core-api/src/modules/tags/db/definitions/tags.ts(1 hunks)backend/core-api/src/modules/tags/db/models/Tags.ts(5 hunks)backend/core-api/src/modules/tags/graphql/queries.ts(4 hunks)backend/core-api/src/modules/tags/graphql/schemas.ts(3 hunks)backend/erxes-api-shared/src/core-types/modules/tags/tag.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- backend/erxes-api-shared/src/core-types/modules/tags/tag.ts
- backend/core-api/src/commands/migrateTags.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx}: Use functional and declarative programming patterns; avoid classes.
Use TypeScript for all code; prefer interfaces over types.
Avoid enums; use maps instead.
Files:
backend/core-api/src/modules/tags/graphql/queries.tsbackend/core-api/src/modules/tags/db/definitions/tags.tsbackend/core-api/src/modules/tags/db/models/Tags.tsbackend/core-api/src/modules/tags/graphql/schemas.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Avoid console logs.
Always use absolute paths when importing.
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
Use absolute path when import.
Files:
backend/core-api/src/modules/tags/graphql/queries.tsbackend/core-api/src/modules/tags/db/definitions/tags.tsbackend/core-api/src/modules/tags/db/models/Tags.tsbackend/core-api/src/modules/tags/graphql/schemas.ts
🧬 Code graph analysis (2)
backend/core-api/src/modules/tags/graphql/queries.ts (2)
backend/core-api/src/modules/tags/@types/tag.ts (1)
ITagFilterQueryParams(6-15)backend/erxes-api-shared/src/utils/service-discovery.ts (2)
getPlugins(20-25)getPlugin(30-49)
backend/core-api/src/modules/tags/db/models/Tags.ts (3)
backend/erxes-api-shared/src/core-types/modules/tags/tag.ts (1)
ITag(3-12)backend/core-api/src/modules/tags/utils.ts (2)
setRelatedTagIds(6-35)removeRelatedTagIds(38-71)backend/erxes-api-shared/src/utils/utils.ts (1)
escapeRegExp(133-135)
🪛 ast-grep (0.38.6)
backend/core-api/src/modules/tags/graphql/queries.ts
[warning] 26-26: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(.*${searchValue}.*, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: payment_ui-ci
- GitHub Check: sales_api-ci
- GitHub Check: Sourcery review
🔇 Additional comments (1)
backend/core-api/src/modules/tags/graphql/schemas.ts (1)
28-36: Confirm intent to keep “type” in query/mutation params.PR summary mentions removing legacy type; these SDLs still expose it. Align with the chosen direction (keep or remove) and sync resolvers.
Also applies to: 49-55, 58-59
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important
Looks good to me! 👍
Reviewed 14b5c5e in 1 minute and 42 seconds. Click for details.
- Reviewed
274lines of code in7files - Skipped
0files when reviewing. - Skipped posting
6draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. backend/core-api/src/modules/tags/graphql/schemas.ts:60
- Draft comment:
Added required 'type' parameter to the tagsTag mutation. Ensure the resolver validates this new parameter and update client docs if this change is breaking. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% The comment has two parts: 1) noting the change (which is just stating the obvious) and 2) asking to "ensure" validation and doc updates. This is exactly the kind of verification/confirmation comment we're supposed to remove. The author likely knows they need to handle the new parameter appropriately. Maybe the validation reminder is actually important since this is a breaking API change? Maybe we should keep comments about breaking changes? No - the rules explicitly say not to make comments asking authors to verify things or update docs, even for breaking changes. We should trust the author to handle this properly. This comment should be removed as it merely states the obvious and asks for verification/documentation updates, which violates our rules.
2. frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx:309
- Draft comment:
The destructuring now omits the legacy 'hasChildren' and the RecordTableTree.Trigger now uses only 'isGroup' for hasChildren. Verify that this change is aligned with the new tag domain logic. - Reason this comment was not posted:
Comment did not seem useful. Confidence is useful =40%<= threshold50%The comment is asking the author to verify if the change aligns with the new tag domain logic. This is asking for confirmation of intention, which is against the rules. However, it does point out a specific change in the code, which could be useful for the author to consider. The comment is borderline, but it leans towards being a violation because it primarily asks for verification rather than suggesting a specific code change or improvement.
3. frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx:59
- Draft comment:
In the 'adding-tag-to-group' mode, the new tag's order is generated as${tag.order}new-item-temp/. Confirm that the concatenation includes intended separators and that a default value for 'tag.order' is ensured. - Reason this comment was not posted:
Comment looked like it was already resolved.
4. frontend/libs/erxes-ui/src/modules/filter/components/Filter.tsx:114
- Draft comment:
Removal of the useScopedHotkeys hook (removed hotkey registration for 'f') may affect filter accessibility. Confirm that this is an intentional change and that hotkey functionality is addressed elsewhere if needed. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 0% vs. threshold = 50% The presence of the Kbd component and hotkey-related UI suggests this was an intentional change. The code still handles hotkey scopes via usePreviousHotkeyScope, suggesting the hotkey functionality may have been moved elsewhere rather than removed entirely. Without seeing the full codebase, I can't be certain if this is a real issue or if the hotkey is now handled at a different level. I may be missing important context about the hotkey system architecture. The presence of hotkey UI elements could be misleading if they're actually defunct now. However, this kind of speculative "verify that..." comment is explicitly against our review rules. We should only keep comments that point out definite issues. The comment should be deleted as it's speculative and asks for verification rather than pointing out a definite issue. If there is a real hotkey problem, it should manifest in testing.
5. frontend/libs/ui-modules/src/modules/tags/types/Tag.ts:12
- Draft comment:
The 'isGroup' and 'order' properties have been made required. Ensure that all tag objects have these fields populated or that a migration/defaults are in place to meet the new interface. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% This comment violates several rules. It's asking to "ensure" something, which is explicitly discouraged. It's speculative about potential migration needs. It's not pointing out a clear code change required. TypeScript will already catch any missing required fields at compile time, so this warning isn't necessary. Maybe this comment is valuable because it proactively prevents runtime errors and helps with backwards compatibility? Maybe the migration concern is legitimate? While migration concerns might be valid, they would be caught by TypeScript compilation errors wherever these interfaces are used. This is exactly the kind of thing we should let the build process catch rather than commenting about. Delete this comment. It violates our rules by being speculative and asking for verification. TypeScript's type system will naturally enforce these requirements.
6. frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx:59
- Draft comment:
In the new code on line 59, the order property is set as${tag.order}new-item-temp/. It seems like a slash might be missing between${tag.order}andnew-item-temp. If the intention is to have a format like 'groupName/new-item-temp', consider changing it to${tag.order}/new-item-temp. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 20% vs. threshold = 50% The old code used a slash separator between group name and child name. However, in the new code, we don't know the exact format of tag.order - it could already include a trailing slash. Without seeing how tag.order is formatted elsewhere in the codebase, we can't be certain that adding a slash is correct. The comment is speculative without full context. I might be wrong about the order format - maybe there's a clear convention throughout the codebase that requires this slash. The old code's format could be a strong indication that this is indeed a bug. While the old code is suggestive, we can't be certain about the order format without seeing how tag.order is used elsewhere. Making assumptions about string formats without full context could introduce bugs rather than fix them. The comment should be deleted because it makes assumptions about string formatting without sufficient context to be certain there's an actual issue.
Workflow ID: wflow_MK0l5aBoxR8S3CIB
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (6)
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx (1)
299-317: Fix hook order: move hooks above early return (Rules of Hooks).Hooks are invoked after a conditional return; this violates hook ordering and triggers linter errors. Hoist all hooks before the early return and keep the branch as a simple render guard.
- const { mode } = useTagContext(); - const row = cell.row.original; - - if ( - (mode === 'adding-tag' || - mode === 'adding-group' || - mode === 'adding-tag-to-group') && - row._id === 'new-item-temp' - ) { - return <NewItemCell />; - } - - const { tagsEdit, loading } = useTagsEdit(); - const { _id, name, isGroup, order } = row; - const [editingTagId, setEditingTagId] = useQueryState('editingTagId'); - const [open, setOpen] = React.useState<boolean>(false); - const [_name, setName] = React.useState<string>(name); + const { mode } = useTagContext(); + const row = cell.row.original; + const { tagsEdit, loading } = useTagsEdit(); + const { _id, name, isGroup, order, hasChildren } = row; + const [editingTagId, setEditingTagId] = useQueryState('editingTagId'); + const [open, setOpen] = React.useState<boolean>(false); + const [_name, setName] = React.useState<string>(name); + + if ( + (mode === 'adding-tag' || + mode === 'adding-group' || + mode === 'adding-tag-to-group') && + row._id === 'new-item-temp' + ) { + return <NewItemCell />; + }backend/core-api/src/modules/tags/graphql/schemas.ts (5)
28-36: DroptypefromqueryParamsand keep new filters only.Aligns with “remove legacy type handling.” Also ensures parity with clients.
Apply this diff:
const queryParams = ` - type: String, searchValue: String, parentId: String, ids: [String], excludeIds: Boolean, isGroup: Boolean, instanceId: String, @@ `;
42-47: AligntagsQueryCountsignature withtagsfilters.Keep params consistent; remove
typeand add missing filters.Apply this diff:
export const queries = ` tagsGetTypes: [JSON] tags(${queryParams}): TagsListResponse tagDetail(_id: String!): Tag - tagsQueryCount(type: String, searchValue: String): Int + tagsQueryCount( + searchValue: String, + parentId: String, + ids: [String], + excludeIds: Boolean, + isGroup: Boolean, + instanceId: String + ): Int `;
49-55: Remove legacytypefrom mutation params.Creation/edit flows should no longer accept it.
Apply this diff:
const mutationParams = ` - type: String, colorCode: String, parentId: String, isGroup: Boolean, description: String, `;
4-19: Legacy fields kept on Tag (type, order) contradict PR goal. Remove or deprecate.PR says “remove legacy type and order.” They’re still exposed here.
Apply this diff (remove):
type Tag @key(fields: "_id") @cacheControl(maxAge: 3) { @@ - type: String - - order: String @@ }If you need a transition, mark them deprecated instead:
- type: String + type: String @deprecated(reason: "Legacy; scheduled for removal") @@ - order: String + order: String @deprecated(reason: "Legacy; scheduled for removal")
58-62: FixtagsTagsignature and return type.If
typeis removed system‑wide, drop it here and return a typed payload.Apply this diff:
-export const mutations = ` - tagsAdd(name: String!, ${mutationParams}): Tag - tagsEdit(_id: String!, name: String, ${mutationParams}): Tag - tagsTag(type: String!, targetIds: [String!]!, tagIds: [String!]!): JSON - tagsRemove(_id: String!): JSON -`; +export const types = ` + ${types} + + type TagTagResult { + modifiedCount: Int! + skippedCount: Int! + } +`; + +export const mutations = ` + tagsAdd(name: String!, ${mutationParams}): Tag + tagsEdit(_id: String!, name: String, ${mutationParams}): Tag + tagsTag(targetIds: [String!]!, tagIds: [String!]!): TagTagResult + tagsRemove(_id: String!): JSON +`;If you must keep
typetemporarily, mark it deprecated in SDL and plan removal in the next minor.
🧹 Nitpick comments (10)
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx (3)
201-212: Await deletion and remove console usage; surface failure via toast.Current code doesn’t await removeTag and logs to console. Follow repo guideline “avoid console logs.”
- const onRemove = () => { - confirm({ - message: 'Are you sure you want to remove the selected?', - options: confirmOptions, - }).then(async () => { - try { - removeTag(_id); - } catch (e) { - console.error(e.message); - } - }); - }; + const onRemove = () => { + confirm({ + message: 'Are you sure you want to remove the selected?', + options: confirmOptions, + }).then(async () => { + const ok = await removeTag(_id); + if (!ok) { + toast({ title: 'Failed to remove tag', variant: 'destructive' }); + } + }); + };
133-139: Remove console.log in error handler.Per guidelines, avoid console logs; toast already reports the error.
- onError: (error) => { - console.log('error', error); - toast({ - title: error.message, - variant: 'destructive', - }); - }, + onError: (error) => { + toast({ title: error.message, variant: 'destructive' }); + },
442-459: Tiny nit: extract formatDate to a top-level pure function.Avoid re-creating the helper on every cell render; also aligns with “function keyword for pure functions.”
- cell: ({ cell }) => { - const formatDate = (dateString: string) => { + cell: ({ cell }) => { + function formatDate(dateString: string) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); - }; + }frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx (2)
50-61: Use a stable, delimiter-safe placeholder order key.Appending “/” risks parsing/sorting edge cases. Prefer a stable suffix that doesn’t rely on reserved separators.
- order: `${tag.order}new-item-temp/`, + // Keep lexical ordering under the group without introducing "/" semantics + order: `${tag.order}T:__new__#new-item-temp`,
44-49: Avoid recomputing hasChildren if hook already provides it.useTags returns tags with hasChildren; you can pass through tag.hasChildren directly.
- existingTags.forEach((tag) => { - result.push({ - ...tag, - hasChildren: existingTags.some(t => t.parentId === tag._id) && tag.isGroup, - }); + existingTags.forEach((tag) => { + result.push(tag);frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx (2)
34-38: Prepare to drop legacy fields from selection (type, order).PR intent says “remove legacy type and order.” Frontend still selects both; this will break once backend removes/deprecates them. Remove now or guard behind a fragment/flag.
Apply this diff:
objectCount - type description isGroup createdAt - order
26-46: Optional: centralize tag fields via a GraphQL fragment.Reduces drift across queries.
Example:
+export const TAG_FIELDS = gql` + fragment TagFields on Tag { + _id + colorCode + name + parentId + totalObjectCount + objectCount + description + isGroup + createdAt + } +`; + export const TAGS_QUERY = gql` query Tags( @@ ) { tags( @@ ) { list { - _id - colorCode - name - parentId - totalObjectCount - objectCount - description - isGroup - createdAt - order + ...TagFields } @@ } } + ${TAG_FIELDS} `;frontend/libs/ui-modules/src/modules/tags/components/SelectTags.tsx (3)
146-166: Avoid O(N^2) filtering when rendering groups.Multiple nested
filter/somescans scale poorly. Precompute a parent→children map once.Apply this diff:
- {tags - ?.filter( - (tag) => tag.isGroup && tags.some((t) => t.parentId === tag._id), - ) - ?.map((tag) => ( - <Command.Group key={tag._id} heading={tag.name}> - {tags - .filter((t) => t.parentId === tag._id) - .map((childTag) => ( - <SelectTagsItem - key={childTag._id} - tag={{ - ...childTag, - hasChildren: tags.some( - (t) => t.parentId === childTag._id, - ), - }} - /> - ))} - </Command.Group> - ))} + {(() => { + const byParent = new Map<string, ITag[]>(); + for (const t of tags ?? []) { + if (!t.parentId) continue; + const arr = byParent.get(t.parentId) || []; + arr.push(t); + byParent.set(t.parentId, arr); + } + return (tags ?? []) + .filter((g) => g.isGroup && (byParent.get(g._id) || []).length) + .map((g) => ( + <Command.Group key={g._id} heading={g.name}> + {(byParent.get(g._id) || []).map((child) => ( + <SelectTagsItem + key={child._id} + tag={{ ...child, hasChildren: !!byParent.get(child._id)?.length }} + /> + ))} + </Command.Group> + )); + })()}
200-216:hasChildrenprop is unused in SelectTagsItem.It’s passed in but never read. Drop it to simplify the API.
Apply this diff:
-export const SelectTagsItem = ({ - tag, -}: { - tag: ITag & { hasChildren: boolean }; -}) => { +export const SelectTagsItem = ({ tag }: { tag: ITag }) => { const { onSelect, selectedTags } = useSelectTagsContext(); const isSelected = selectedTags.some((t) => t._id === tag._id); return ( <Command.Item onSelect={() => onSelect(tag)}> <TextOverflowTooltip value={tag.name} className="flex-auto w-auto font-medium" /> <Combobox.Check checked={isSelected} /> </Command.Item> ); };Also remove the
hasChildrenconstruction at lines 141–143 and 158–163.
1-23: Prefer absolute import aliases per guidelines.Convert local relative imports to your workspace alias (e.g.,
ui-modules/...).Example:
-import { useTags } from '../hooks/useTags'; +import { useTags } from 'ui-modules/modules/tags/hooks/useTags'; @@ -import { SelectTagsContext } from '../contexts/SelectTagsContext'; -import { useSelectTagsContext } from '../hooks/useSelectTagsContext'; +import { SelectTagsContext } from 'ui-modules/modules/tags/contexts/SelectTagsContext'; +import { useSelectTagsContext } from 'ui-modules/modules/tags/hooks/useSelectTagsContext'; @@ -import { CreateTagForm, SelectTagCreateContainer } from './CreateTagForm'; -import { TagBadge } from './TagBadge'; +import { CreateTagForm, SelectTagCreateContainer } from 'ui-modules/modules/tags/components/CreateTagForm'; +import { TagBadge } from 'ui-modules/modules/tags/components/TagBadge';
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
backend/core-api/src/modules/tags/graphql/schemas.ts(3 hunks)frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx(1 hunks)frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx(1 hunks)frontend/libs/erxes-ui/src/modules/filter/components/Filter.tsx(0 hunks)frontend/libs/ui-modules/src/modules/tags/components/SelectTags.tsx(3 hunks)frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx(1 hunks)frontend/libs/ui-modules/src/modules/tags/types/Tag.ts(1 hunks)
💤 Files with no reviewable changes (1)
- frontend/libs/erxes-ui/src/modules/filter/components/Filter.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/libs/ui-modules/src/modules/tags/types/Tag.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx}: Use functional and declarative programming patterns; avoid classes.
Use TypeScript for all code; prefer interfaces over types.
Avoid enums; use maps instead.
Files:
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsxfrontend/libs/ui-modules/src/modules/tags/components/SelectTags.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsxbackend/core-api/src/modules/tags/graphql/schemas.tsfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Avoid console logs.
Always use absolute paths when importing.
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
**/*.{ts,tsx,js,jsx}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Use the "function" keyword for pure functions.
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
Use absolute path when import.
Files:
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsxfrontend/libs/ui-modules/src/modules/tags/components/SelectTags.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsxbackend/core-api/src/modules/tags/graphql/schemas.tsfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{tsx,jsx}: Favor named exports for components.
Use declarative JSX.
Use Shadcn UI, Radix, and Tailwind for components and styling.
Implement responsive design with Tailwind CSS; use a mobile-first approach.
**/*.{tsx,jsx}: Structure files: exported component, subcomponents, helpers, static content, types.
Favor named exports for components.
Use declarative JSX.
Use Shadcn UI, Radix, and Tailwind for components and styling.
Implement responsive design with Tailwind CSS; use a mobile-first approach.
Files:
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsxfrontend/libs/ui-modules/src/modules/tags/components/SelectTags.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsxfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx
**/*.tsx
📄 CodeRabbit inference engine (.cursorrules)
**/*.tsx: Use functional components with TypeScript interfaces.
Minimize 'useEffect', and 'setState'.
Wrap client components in Suspense with fallback.
Use dynamic loading for non-critical components.
Limit 'use client': Favor server components and Next.js SSR. Use only for Web API access in small components. Avoid for data fetching or state management.
Files:
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsxfrontend/libs/ui-modules/src/modules/tags/components/SelectTags.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsxfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx
**/*.{webp,tsx,jsx}
📄 CodeRabbit inference engine (.cursorrules)
Optimize images: use WebP format, include size data, implement lazy loading.
Files:
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsxfrontend/libs/ui-modules/src/modules/tags/components/SelectTags.tsxfrontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsxfrontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx
🧬 Code graph analysis (4)
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx (15)
frontend/libs/ui-modules/src/modules/tags/hooks/useTags.tsx (1)
useTags(9-57)frontend/libs/erxes-ui/src/components/popover.tsx (1)
Popover(52-57)frontend/libs/erxes-ui/src/components/command.tsx (1)
Command(223-233)frontend/core-ui/src/modules/settings/tags/hooks/useTagsAdd.tsx (1)
useTagsAdd(5-27)frontend/libs/ui-modules/src/modules/tags/types/Tag.ts (2)
ITagQueryResponse(20-25)ITag(3-18)frontend/libs/erxes-ui/src/hooks/use-confirm.tsx (1)
useConfirm(133-181)frontend/libs/erxes-ui/src/hooks/use-query-state.tsx (1)
useQueryState(6-49)frontend/core-ui/src/modules/settings/tags/hooks/useRemoveTag.tsx (1)
useRemoveTag(5-30)frontend/core-ui/src/modules/settings/tags/hooks/useTagsEdit.tsx (1)
useTagsEdit(5-41)frontend/libs/erxes-ui/src/modules/record-table/index.ts (1)
RecordTable(23-40)frontend/libs/erxes-ui/src/components/combobox.tsx (1)
Combobox(200-208)frontend/libs/erxes-ui/src/modules/record-table/components/RecordTableCellInline.tsx (1)
RecordTableInlineCell(100-104)frontend/libs/erxes-ui/src/modules/record-table/components/RecordTableTree.tsx (1)
RecordTableTree(131-136)frontend/libs/erxes-ui/src/components/text-overflow.tsx (1)
TextOverflowTooltip(10-56)frontend/libs/erxes-ui/src/components/textarea.tsx (1)
Textarea(5-19)
frontend/libs/ui-modules/src/modules/tags/components/SelectTags.tsx (3)
frontend/libs/erxes-ui/src/components/command.tsx (1)
Command(223-233)frontend/libs/erxes-ui/src/components/text-overflow.tsx (1)
TextOverflowTooltip(10-56)frontend/libs/erxes-ui/src/components/combobox.tsx (1)
Combobox(200-208)
frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx (5)
frontend/core-ui/src/modules/settings/tags/providers/TagProvider.tsx (1)
useTagContext(45-47)frontend/libs/erxes-ui/src/hooks/use-query-state.tsx (1)
useMultiQueryState(57-98)frontend/libs/ui-modules/src/modules/tags/hooks/useTags.tsx (1)
useTags(9-57)frontend/libs/ui-modules/src/modules/tags/types/Tag.ts (1)
ITag(3-18)frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx (1)
tagsColumns(293-472)
backend/core-api/src/modules/tags/graphql/schemas.ts (5)
backend/core-api/src/modules/organization/team-member/graphql/schema.ts (1)
mutations(179-201)backend/core-api/src/modules/documents/graphql/schema.ts (1)
mutations(70-73)backend/core-api/src/modules/contacts/graphql/schemas/company.ts (1)
mutations(123-128)backend/core-api/src/modules/products/graphql/schemas/product.ts (2)
mutations(121-127)mutationParams(99-119)backend/core-api/src/modules/organization/brand/graphql/schema.ts (1)
mutations(43-47)
🪛 Biome (2.1.2)
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx
[error] 311-311: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 313-313: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 314-314: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 315-315: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 317-317: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 321-321: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
- GitHub Check: payment_api-ci
- GitHub Check: frontline_ui-ci
- GitHub Check: frontline_api-ci
- GitHub Check: core-api-ci
- GitHub Check: tourism_api-ci
- GitHub Check: core-ui
- GitHub Check: operation_api-ci
- GitHub Check: sales_api-ci
- GitHub Check: logs-ci
- GitHub Check: gateway-ci
- GitHub Check: tourism_ui-ci
- GitHub Check: automations-ci
- GitHub Check: sales_ui-ci
- GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (4)
frontend/core-ui/src/modules/settings/tags/components/TagsRecordTable.tsx (1)
68-93: LGTM on data flow and virtualization.Memoized transform + RecordTableTree ordered rendering is clean and matches the new isGroup/parentId model.
frontend/libs/ui-modules/src/modules/tags/graphql/queries/tagsQueries.tsx (1)
11-14: Good: new filters wired (ids, excludeIds, isGroup).Args and usage align with backend queryParams. No issues spotted.
Also applies to: 19-25
frontend/libs/ui-modules/src/modules/tags/components/SelectTags.tsx (1)
134-145: Top-level non-group items: confirm UX and intent.You exclude groups and render only non-group, parentless items. Empty groups will never be shown. Confirm this is desired.
backend/core-api/src/modules/tags/graphql/schemas.ts (1)
42-62: 'type' still present — schema, resolver, and clients must be reconciledThe mutation signature and resolver still accept
typeand multiple front-end callers still send it; either keeptypein the schema/resolver or remove every usage in backend and clients.
- backend/core-api/src/modules/tags/graphql/schemas.ts: line 60 —
tagsTag(type: String!, targetIds: [String!]!, tagIds: [String!]!): JSON- backend/core-api/src/modules/tags/graphql/mutations.ts: lines 27–30 —
async tagsTag(..., { type, ... })(resolver destructurestype)- frontend/plugins/sales_ui/src/modules/deals/cards/components/detail/overview/tags/SelectTags.tsx: lines 53–55 —
giveTags({ variables: { type: 'sales:deal', ... } })- frontend/libs/ui-modules/src/modules/tags/graphql/mutations/tagsMutations.ts: lines 23–29 — mutation defines
$type: String!and callstagsTag(type: $type, ...)- frontend/core-ui/src/modules/settings/tags/graphql/mutations/tagsMutations.ts: lines 50–56 — same
$typeusageLikely an incorrect or invalid review comment.
| <RecordTableInlineCell.Trigger> | ||
| <RecordTableTree.Trigger | ||
| order={order || ''} | ||
| name={cell.getValue() as string} | ||
| hasChildren={isGroup} | ||
| > | ||
| <div className="flex items-center gap-2 min-w-0"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use computed hasChildren; passing isGroup breaks tree arrows/expandability.
The tree trigger should reflect actual children, not group-ness.
- <RecordTableTree.Trigger
+ <RecordTableTree.Trigger
order={order || ''}
name={cell.getValue() as string}
- hasChildren={isGroup}
+ hasChildren={hasChildren}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <RecordTableInlineCell.Trigger> | |
| <RecordTableTree.Trigger | |
| order={order || ''} | |
| name={cell.getValue() as string} | |
| hasChildren={isGroup} | |
| > | |
| <div className="flex items-center gap-2 min-w-0"> | |
| <RecordTableInlineCell.Trigger> | |
| <RecordTableTree.Trigger | |
| order={order || ''} | |
| name={cell.getValue() as string} | |
| hasChildren={hasChildren} | |
| > | |
| <div className="flex items-center gap-2 min-w-0"> |
🤖 Prompt for AI Agents
frontend/core-ui/src/modules/settings/tags/components/TagsColumns.tsx around
lines 355 to 361: RecordTableTree.Trigger is being passed hasChildren={isGroup},
which breaks tree arrows/expandability; replace that with a computed boolean
derived from the row (for example use row.getCanExpand() or check
row.getLeafRows().length > 0 / row.getIsGrouped() as appropriate) and pass that
computed value to hasChildren, removing the direct use of isGroup.
KhBaterdene
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
front end tal ni ok
Summary by Sourcery
Refactor the tags domain by removing legacy type and order fields, introducing an isGroup flag, centralizing validation logic, simplifying create/update/remove flows, updating the GraphQL schema and mutations, and adding data migration scripts.
New Features:
Enhancements:
Chores:
Summary by CodeRabbit
New Features
Bug Fixes / Data Migration
Refactor
Chores
Important
Refactor tags domain by adding
isGroupattribute, centralizing validation, updating GraphQL schema, and enhancing UI for better tag management.isGroupattribute to distinguish between group and item tags.tagsTagmutation for bulk tagging.schemas.tsto include new fields likeisGroupanddescription.tagsMutations.tsandtagsQueries.tsto handle new tag attributes and operations.Tags.tsto enforce unique names and group constraints.tagSchemaintags.tsto enforce unique names and streamline indexes.customer.tsto include tagging actions.migrateTags.tsto update existing tags with new structure.migrateContentType.tsto close DB client on completion.TagsColumns.tsxandTagsGroupsAddButtons.tsxfor improved tag management UI.TagsRecordTable.tsxto handle new tag attributes and operations.TagProvider.tsxfor context management in tag operations.This description was created by
for 14b5c5e. You can customize this summary. It will automatically update as commits are pushed.